"use client"; import { motion, useInView, useReducedMotion } from "framer-motion"; import { useEffect, useMemo, useRef, useState } from "react"; interface TextDiffuseRevealProps { text: string; className?: string; startDelay?: number; once?: boolean; amount?: number; } const EXPO_OUT = [0.16, 1, 0.3, 1] as const; export function TextDiffuseReveal({ text, className = "", startDelay = 0, once = true, amount = 0.3, }: Readonly) { const ref = useRef(null); const shouldReduceMotion = useReducedMotion(); const isInView = useInView(ref, { once, amount }); const [staggerDelay, setStaggerDelay] = useState(0.03); const [completedChars, setCompletedChars] = useState(0); useEffect(() => { const media = window.matchMedia("(max-width: 768px)"); const updateStagger = () => setStaggerDelay(media.matches ? 0.015 : 0.03); updateStagger(); media.addEventListener("change", updateStagger); return () => media.removeEventListener("change", updateStagger); }, []); const tokens = useMemo(() => text.split(/(\n|\s+)/), [text]); const animatedChars = useMemo( () => Array.from(text).filter((char) => char !== "\n" && char !== " ").length, [text], ); useEffect(() => { if (!isInView) { setCompletedChars(0); } }, [isInView]); const hasActiveWillChange = !shouldReduceMotion && isInView && completedChars < animatedChars; let animatedIndex = 0; return ( {tokens.map((token, tokenIndex) => { if (!token) { return null; } if (token === "\n") { return
; } if (/^\s+$/.test(token)) { return ( ); } return ( {Array.from(token).map((char, charIndex) => { const currentIndex = animatedIndex; animatedIndex += 1; return ( { if (!shouldReduceMotion) { setCompletedChars((count) => count < animatedChars ? count + 1 : count, ); } }} > {char} ); })} ); })}
); }