|
1 | 1 | "use client" |
2 | | -import { merge } from "@halvaradop/ui-core" |
3 | 2 | import { AnimatePresence, motion } from "motion/react" |
4 | 3 | import type { TargetAndTransition, Transition, Variant, Variants } from "motion/react" |
5 | | -import React from "react" |
6 | | - |
7 | | -export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide" |
8 | | - |
9 | | -export type PerType = "word" | "char" | "line" |
10 | | - |
11 | | -export type TextEffectProps = { |
12 | | - children: string |
13 | | - per?: PerType |
14 | | - as?: keyof React.JSX.IntrinsicElements |
15 | | - variants?: { |
16 | | - container?: Variants |
17 | | - item?: Variants |
18 | | - } |
19 | | - className?: string |
20 | | - preset?: PresetType |
21 | | - delay?: number |
22 | | - speedReveal?: number |
23 | | - speedSegment?: number |
24 | | - trigger?: boolean |
25 | | - onAnimationComplete?: () => void |
26 | | - onAnimationStart?: () => void |
27 | | - segmentWrapperClassName?: string |
28 | | - containerTransition?: Transition |
29 | | - segmentTransition?: Transition |
30 | | - style?: React.CSSProperties |
31 | | -} |
32 | | - |
33 | | -const defaultStaggerTimes: Record<PerType, number> = { |
34 | | - char: 0.03, |
35 | | - word: 0.05, |
36 | | - line: 0.1, |
37 | | -} |
38 | | - |
39 | | -const defaultContainerVariants: Variants = { |
40 | | - hidden: { opacity: 0 }, |
41 | | - visible: { |
42 | | - opacity: 1, |
43 | | - transition: { |
44 | | - staggerChildren: 0.05, |
45 | | - }, |
46 | | - }, |
47 | | - exit: { |
48 | | - transition: { staggerChildren: 0.05, staggerDirection: -1 }, |
49 | | - }, |
50 | | -} |
51 | | - |
52 | | -const defaultItemVariants: Variants = { |
53 | | - hidden: { opacity: 0 }, |
54 | | - visible: { |
55 | | - opacity: 1, |
56 | | - }, |
57 | | - exit: { opacity: 0 }, |
58 | | -} |
59 | | - |
60 | | -const presetVariants: Record<PresetType, { container: Variants; item: Variants }> = { |
61 | | - blur: { |
62 | | - container: defaultContainerVariants, |
63 | | - item: { |
64 | | - hidden: { opacity: 0, filter: "blur(12px)" }, |
65 | | - visible: { opacity: 1, filter: "blur(0px)" }, |
66 | | - exit: { opacity: 0, filter: "blur(12px)" }, |
67 | | - }, |
68 | | - }, |
69 | | - "fade-in-blur": { |
70 | | - container: defaultContainerVariants, |
71 | | - item: { |
72 | | - hidden: { opacity: 0, y: 20, filter: "blur(12px)" }, |
73 | | - visible: { opacity: 1, y: 0, filter: "blur(0px)" }, |
74 | | - exit: { opacity: 0, y: 20, filter: "blur(12px)" }, |
75 | | - }, |
76 | | - }, |
77 | | - scale: { |
78 | | - container: defaultContainerVariants, |
79 | | - item: { |
80 | | - hidden: { opacity: 0, scale: 0 }, |
81 | | - visible: { opacity: 1, scale: 1 }, |
82 | | - exit: { opacity: 0, scale: 0 }, |
83 | | - }, |
84 | | - }, |
85 | | - fade: { |
86 | | - container: defaultContainerVariants, |
87 | | - item: { |
88 | | - hidden: { opacity: 0 }, |
89 | | - visible: { opacity: 1 }, |
90 | | - exit: { opacity: 0 }, |
91 | | - }, |
92 | | - }, |
93 | | - slide: { |
94 | | - container: defaultContainerVariants, |
95 | | - item: { |
96 | | - hidden: { opacity: 0, y: 20 }, |
97 | | - visible: { opacity: 1, y: 0 }, |
98 | | - exit: { opacity: 0, y: 20 }, |
99 | | - }, |
100 | | - }, |
101 | | -} |
102 | | - |
103 | | -const AnimationComponent: React.FC<{ |
104 | | - segment: string |
105 | | - variants: Variants |
106 | | - per: "line" | "word" | "char" |
107 | | - segmentWrapperClassName?: string |
108 | | -}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => { |
109 | | - const content = |
110 | | - per === "line" ? ( |
111 | | - <motion.span variants={variants} className="block"> |
112 | | - {segment} |
113 | | - </motion.span> |
114 | | - ) : per === "word" ? ( |
115 | | - <motion.span aria-hidden="true" variants={variants} className="inline-block whitespace-pre"> |
116 | | - {segment} |
117 | | - </motion.span> |
118 | | - ) : ( |
119 | | - <motion.span className="inline-block whitespace-pre"> |
120 | | - {segment.split("").map((char, charIndex) => ( |
121 | | - <motion.span |
122 | | - key={`char-${charIndex}`} |
123 | | - aria-hidden="true" |
124 | | - variants={variants} |
125 | | - className="inline-block whitespace-pre" |
126 | | - > |
127 | | - {char} |
128 | | - </motion.span> |
129 | | - ))} |
130 | | - </motion.span> |
131 | | - ) |
132 | | - |
133 | | - if (!segmentWrapperClassName) { |
134 | | - return content |
135 | | - } |
136 | | - |
137 | | - const defaultWrapperClassName = per === "line" ? "block" : "inline-block" |
138 | | - |
139 | | - return <span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>{content}</span> |
140 | | -}) |
141 | | - |
142 | | -AnimationComponent.displayName = "AnimationComponent" |
| 4 | +import { TextEffectProps } from "@/lib/@types/props" |
| 5 | +import type { PerType } from "@/lib/@types/types" |
| 6 | +import { |
| 7 | + defaultContainerVariants, |
| 8 | + defaultItemVariants, |
| 9 | + defaultStaggerTimes, |
| 10 | + presetVariants, |
| 11 | +} from "@/ui/motion/text-effect.motion" |
| 12 | +import { AnimationComponent } from "@/ui/animation-component" |
143 | 13 |
|
144 | 14 | const splitText = (text: string, per: PerType) => { |
145 | 15 | if (per === "line") return text.split("\n") |
|
0 commit comments