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