'use client'; import { type JSX, useEffect, useState } from 'react'; import { motion, MotionProps } from 'motion/react'; export type TextScrambleProps = { children: string; duration?: number; speed?: number; characterSet?: string; as?: React.ElementType; className?: string; trigger?: boolean; onScrambleComplete?: () => void; } & MotionProps; const defaultChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; export function TextScramble({ children, duration = 0.8, speed = 0.04, characterSet = defaultChars, className, as: Component = 'p', trigger = true, onScrambleComplete, ...props }: TextScrambleProps) { const MotionComponent = motion.create( Component as keyof JSX.IntrinsicElements ); const [displayText, setDisplayText] = useState(children); const [isAnimating, setIsAnimating] = useState(false); const text = children; const scramble = async () => { if (isAnimating) return; setIsAnimating(true); const steps = duration / speed; let step = 0; const interval = setInterval(() => { let scrambled = ''; const progress = step / steps; for (let i = 0; i < text.length; i++) { if (text[i] === ' ') { scrambled += ' '; continue; } if (progress * text.length > i) { scrambled += text[i]; } else { scrambled += characterSet[Math.floor(Math.random() * characterSet.length)]; } } setDisplayText(scrambled); step++; if (step > steps) { clearInterval(interval); setDisplayText(text); setIsAnimating(false); onScrambleComplete?.(); } }, speed * 1000); }; useEffect(() => { if (!trigger) return; scramble(); }, [trigger]); return ( <MotionComponent className={className} {...props}> {displayText} </MotionComponent> ); }