import { useSprings, animated, SpringConfig } from '@react-spring/web'; import { useEffect, useRef, useState } from 'react'; interface SplitTextProps { text?: string; className?: string; delay?: number; animationFrom?: { opacity: number; transform: string }; animationTo?: { opacity: number; transform: string }; easing?: SpringConfig['easing']; threshold?: number; rootMargin?: string; textAlign?: 'left' | 'right' | 'center' | 'justify' | 'start' | 'end'; onLetterAnimationComplete?: () => void; onLineCountChange?: (lineCount: number) => void; } const splitGraphemes = (text: string): string[] => { if (typeof Intl !== 'undefined' && Intl.Segmenter) { const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); const segments = segmenter.segment(text); return Array.from(segments).map((s) => s.segment); } else { // Fallback for browsers without Intl.Segmenter return [...text]; } }; const SplitText: React.FC = ({ text = '', className = '', delay = 100, animationFrom = { opacity: 0, transform: 'translate3d(0,40px,0)' }, animationTo = { opacity: 1, transform: 'translate3d(0,0,0)' }, easing = (t: number) => t, threshold = 0.1, rootMargin = '-100px', textAlign = 'center', onLetterAnimationComplete, onLineCountChange, }) => { const words = text.split(' ').map(splitGraphemes); const letters = words.flat(); const [inView, setInView] = useState(false); const ref = useRef(null); const animatedCount = useRef(0); const springs = useSprings( letters.length, letters.map((_, i) => ({ from: animationFrom, to: inView ? async (next: (props: any) => Promise) => { await next(animationTo); animatedCount.current += 1; if (animatedCount.current === letters.length && onLetterAnimationComplete) { onLetterAnimationComplete(); } } : animationFrom, delay: i * delay, config: { easing }, })), ); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setInView(true); if (ref.current) { observer.unobserve(ref.current); } } }, { threshold, rootMargin }, ); if (ref.current) { observer.observe(ref.current); } return () => observer.disconnect(); }, [threshold, rootMargin]); useEffect(() => { if (ref.current && inView) { const element = ref.current; setTimeout(() => { const lineHeight = parseInt(getComputedStyle(element).lineHeight) || parseInt(getComputedStyle(element).fontSize) * 1.2; const height = element.offsetHeight; const lines = Math.round(height / lineHeight); if (onLineCountChange) { onLineCountChange(lines); } }, 100); } }, [inView, text, onLineCountChange]); return ( <> {text} ); }; export default SplitText;