'use client'; import * as React from 'react'; import { motion, useInView, type UseInViewOptions } from 'motion/react'; import { cn } from '@workspace/ui/lib/utils'; function CursorBlinker({ className }: { className?: string }) { return ( ); } type TypingTextProps = Omit, 'children'> & { duration?: number; delay?: number; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; cursor?: boolean; loop?: boolean; holdDelay?: number; text: string | string[]; cursorClassName?: string; animateOnChange?: boolean; }; function TypingText({ ref, duration = 100, delay = 0, inView = false, inViewMargin = '0px', inViewOnce = true, cursor = false, loop = false, holdDelay = 1000, text, cursorClassName, animateOnChange = true, ...props }: TypingTextProps) { const localRef = React.useRef(null); React.useImperativeHandle(ref, () => localRef.current as HTMLSpanElement); const inViewResult = useInView(localRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; const [started, setStarted] = React.useState(false); const [displayedText, setDisplayedText] = React.useState(''); React.useEffect(() => { // Reset animation when text changes (if animateOnChange is true) if (animateOnChange) { setStarted(false); setDisplayedText(''); } if (isInView) { const timeoutId = setTimeout(() => { setStarted(true); }, delay); return () => clearTimeout(timeoutId); } else { const timeoutId = setTimeout(() => { setStarted(true); }, delay); return () => clearTimeout(timeoutId); } }, [isInView, delay, ...(animateOnChange ? [text] : [])]); React.useEffect(() => { if (!started) return; const timeoutIds: Array> = []; const texts: string[] = typeof text === 'string' ? [text] : text; const typeText = (str: string, onComplete: () => void) => { let currentIndex = 0; const type = () => { if (currentIndex <= str.length) { setDisplayedText(str.substring(0, currentIndex)); currentIndex++; const id = setTimeout(type, duration); timeoutIds.push(id); } else { onComplete(); } }; type(); }; const eraseText = (str: string, onComplete: () => void) => { let currentIndex = str.length; const erase = () => { if (currentIndex >= 0) { setDisplayedText(str.substring(0, currentIndex)); currentIndex--; const id = setTimeout(erase, duration); timeoutIds.push(id); } else { onComplete(); } }; erase(); }; const animateTexts = (index: number) => { typeText(texts[index] ?? '', () => { const isLast = index === texts.length - 1; if (isLast && !loop) { return; } const id = setTimeout(() => { eraseText(texts[index] ?? '', () => { const nextIndex = isLast ? 0 : index + 1; animateTexts(nextIndex); }); }, holdDelay); timeoutIds.push(id); }); }; animateTexts(0); return () => { timeoutIds.forEach(clearTimeout); }; }, [text, duration, started, loop, holdDelay]); return ( {displayedText} {cursor && } ); } export { TypingText, type TypingTextProps };