1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
'use client';
import { cn } from '@lib/utils';
import { AnimatePresence, motion, type Transition, type Variants } from 'motion/react';
import { useMemo, useId } from 'react';
export type TextMorphProps = {
children: string;
as?: React.ElementType;
className?: string;
style?: React.CSSProperties;
variants?: Variants;
transition?: Transition;
};
export function TextMorph({
children,
as: Component = 'p',
className,
style,
variants,
transition,
}: TextMorphProps) {
const uniqueId = useId();
const characters = useMemo(() => {
const charCounts: Record<string, number> = {};
return children.split('').map((char) => {
const lowerChar = char.toLowerCase();
charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
return {
id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
label: char === ' ' ? '\u00A0' : char,
};
});
}, [children, uniqueId]);
const defaultVariants: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
const defaultTransition: Transition = {
type: 'spring',
stiffness: 280,
damping: 18,
mass: 0.3,
};
return (
// @ts-expect-error - style is optional
<Component className={cn(className)} aria-label={children} style={style}>
<AnimatePresence mode='popLayout' initial={false}>
{characters.map((character) => (
<motion.span
key={character.id}
layoutId={character.id}
className='inline-block'
aria-hidden='true'
initial='initial'
animate='animate'
exit='exit'
variants={variants || defaultVariants}
transition={transition || defaultTransition}
>
{character.label}
</motion.span>
))}
</AnimatePresence>
</Component>
);
}
|