import clsx from 'clsx'; import type { MotionProps } from 'motion/react'; import { motion } from 'motion/react'; import { useEffect, useRef, useState } from 'react'; interface TypingAnimationProps extends MotionProps { as?: React.ElementType; children: string; className?: string; delay?: number; duration?: number; startOnView?: boolean; } export function TypingAnimation({ as: Component = 'div', children, className, delay = 0, duration = 50, startOnView = false, ...props }: TypingAnimationProps) { const MotionComponent = motion.create(Component, { forwardMotionProps: true }); const [displayedText, setDisplayedText] = useState(''); const [started, setStarted] = useState(false); const elementRef = useRef(null); useEffect(() => { if (!startOnView) { const startTimeout = setTimeout(() => { setStarted(true); }, delay); return () => clearTimeout(startTimeout); } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setTimeout(() => { setStarted(true); }, delay); observer.disconnect(); } }, { threshold: 0.1 } ); if (elementRef.current) { observer.observe(elementRef.current); } return () => observer.disconnect(); }, [delay, startOnView]); useEffect(() => { if (!started) return; let i = 0; const typingEffect = setInterval(() => { if (i < children.length) { setDisplayedText(children.substring(0, i + 1)); i += 1; } else { clearInterval(typingEffect); } }, duration); // eslint-disable-next-line consistent-return return () => { clearInterval(typingEffect); }; }, [children, duration, started]); return ( {displayedText} ); }