2025-08-20 04:12:49 -06:00

91 lines
2.4 KiB
TypeScript

'use client';
import * as React from 'react';
import {
motion,
useInView,
type UseInViewOptions,
type Transition,
} from 'motion/react';
const ENTRY_ANIMATION = {
initial: { rotateX: 0 },
animate: { rotateX: 90 },
};
const EXIT_ANIMATION = {
initial: { rotateX: 90 },
animate: { rotateX: 0 },
};
const formatCharacter = (char: string) => (char === ' ' ? '\u00A0' : char);
type RollingTextProps = Omit<React.ComponentProps<'span'>, 'children'> & {
transition?: Transition;
inView?: boolean;
inViewMargin?: UseInViewOptions['margin'];
inViewOnce?: boolean;
text: string;
};
function RollingText({
ref,
transition = { duration: 0.5, delay: 0.1, ease: 'easeOut' },
inView = false,
inViewMargin = '0px',
inViewOnce = true,
text,
...props
}: RollingTextProps) {
const localRef = React.useRef<HTMLSpanElement>(null);
React.useImperativeHandle(ref, () => localRef.current!);
const inViewResult = useInView(localRef, {
once: inViewOnce,
margin: inViewMargin,
});
const isInView = !inView || inViewResult;
const characters = React.useMemo(() => text.split(''), [text]);
return (
<span data-slot="rolling-text" {...props} ref={ref}>
{characters.map((char, idx) => (
<span
key={idx}
className="relative inline-block perspective-[9999999px] transform-3d w-auto"
aria-hidden="true"
>
<motion.span
className="absolute inline-block backface-hidden origin-[50%_25%]"
initial={ENTRY_ANIMATION.initial}
animate={isInView ? ENTRY_ANIMATION.animate : undefined}
transition={{
...transition,
delay: idx * (transition?.delay ?? 0),
}}
>
{formatCharacter(char)}
</motion.span>
<motion.span
className="absolute inline-block backface-hidden origin-[50%_100%]"
initial={EXIT_ANIMATION.initial}
animate={isInView ? EXIT_ANIMATION.animate : undefined}
transition={{
...transition,
delay: idx * (transition?.delay ?? 0) + 0.3,
}}
>
{formatCharacter(char)}
</motion.span>
<span className="invisible">{formatCharacter(char)}</span>
</span>
))}
<span className="sr-only">{text}</span>
</span>
);
}
export { RollingText, type RollingTextProps };