import { AnimatePresence, HTMLMotionProps, TargetAndTransition, Transition, motion, } from 'motion/react'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react'; function cn (...classes: (string | undefined | null | boolean)[]): string { return classes.filter(Boolean).join(' '); } export interface RotatingTextRef { next: () => void previous: () => void jumpTo: (index: number) => void reset: () => void } export interface RotatingTextProps extends Omit< HTMLMotionProps<'span'>, 'children' | 'transition' | 'initial' | 'animate' | 'exit' > { texts: string[] transition?: Transition initial?: TargetAndTransition animate?: TargetAndTransition exit?: TargetAndTransition animatePresenceMode?: 'sync' | 'wait' animatePresenceInitial?: boolean rotationInterval?: number staggerDuration?: number staggerFrom?: 'first' | 'last' | 'center' | 'random' | number loop?: boolean auto?: boolean splitBy?: string onNext?: (index: number) => void mainClassName?: string splitLevelClassName?: string elementLevelClassName?: string } const RotatingText = forwardRef( ( { texts, transition = { type: 'spring', damping: 25, stiffness: 300 }, initial = { y: '100%', opacity: 0 }, animate = { y: 0, opacity: 1 }, exit = { y: '-120%', opacity: 0 }, animatePresenceMode = 'wait', animatePresenceInitial = false, rotationInterval = 2000, staggerDuration = 0, staggerFrom = 'first', loop = true, auto = true, splitBy = 'characters', onNext, mainClassName, splitLevelClassName, elementLevelClassName, ...rest }, ref ) => { const [currentTextIndex, setCurrentTextIndex] = useState(0); const splitIntoCharacters = (text: string): string[] => { return Array.from(text); }; const elements = useMemo(() => { const currentText: string = texts[currentTextIndex]; if (splitBy === 'characters') { const words = currentText.split(' '); return words.map((word, i) => ({ characters: splitIntoCharacters(word), needsSpace: i !== words.length - 1, })); } if (splitBy === 'words') { return currentText.split(' ').map((word, i, arr) => ({ characters: [word], needsSpace: i !== arr.length - 1, })); } if (splitBy === 'lines') { return currentText.split('\n').map((line, i, arr) => ({ characters: [line], needsSpace: i !== arr.length - 1, })); } return currentText.split(splitBy).map((part, i, arr) => ({ characters: [part], needsSpace: i !== arr.length - 1, })); }, [texts, currentTextIndex, splitBy]); const getStaggerDelay = useCallback( (index: number, totalChars: number): number => { const total = totalChars; if (staggerFrom === 'first') return index * staggerDuration; if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration; if (staggerFrom === 'center') { const center = Math.floor(total / 2); return Math.abs(center - index) * staggerDuration; } if (staggerFrom === 'random') { const randomIndex = Math.floor(Math.random() * total); return Math.abs(randomIndex - index) * staggerDuration; } return Math.abs((staggerFrom as number) - index) * staggerDuration; }, [staggerFrom, staggerDuration] ); const handleIndexChange = useCallback( (newIndex: number) => { setCurrentTextIndex(newIndex); if (onNext) onNext(newIndex); }, [onNext] ); const next = useCallback(() => { const nextIndex = currentTextIndex === texts.length - 1 ? loop ? 0 : currentTextIndex : currentTextIndex + 1; if (nextIndex !== currentTextIndex) { handleIndexChange(nextIndex); } }, [currentTextIndex, texts.length, loop, handleIndexChange]); const previous = useCallback(() => { const prevIndex = currentTextIndex === 0 ? loop ? texts.length - 1 : currentTextIndex : currentTextIndex - 1; if (prevIndex !== currentTextIndex) { handleIndexChange(prevIndex); } }, [currentTextIndex, texts.length, loop, handleIndexChange]); const jumpTo = useCallback( (index: number) => { const validIndex = Math.max(0, Math.min(index, texts.length - 1)); if (validIndex !== currentTextIndex) { handleIndexChange(validIndex); } }, [texts.length, currentTextIndex, handleIndexChange] ); const reset = useCallback(() => { if (currentTextIndex !== 0) { handleIndexChange(0); } }, [currentTextIndex, handleIndexChange]); useImperativeHandle( ref, () => ({ next, previous, jumpTo, reset, }), [next, previous, jumpTo, reset] ); useEffect(() => { if (!auto) return; const intervalId = setInterval(next, rotationInterval); return () => clearInterval(intervalId); }, [next, rotationInterval, auto]); return ( {texts[currentTextIndex]} ); } ); RotatingText.displayName = 'RotatingText'; export default RotatingText;