'use client'; import { motion } from 'framer-motion'; import { useEffect, useMemo, useRef, useState, type ElementType } from 'react'; import { usePrefersReducedMotion } from '@/lib/motion'; // Adapted from React Bits' BlurText (reactbits.dev, MIT) — generalised to take // several independently-styled segments instead of one plain string, so a // gold/underlined word can blur in beside plain navy text in the same headline. type Segmento = { text: string; className?: string }; const FROM = { filter: 'blur(10px)', opacity: 0, y: 14 }; const KEYFRAMES = { filter: ['blur(10px)', 'blur(4px)', 'blur(0px)'], opacity: [0, 0.6, 1], y: [14, 4, 0], }; const TIMES = [0, 0.5, 1]; export function TextoDesenfoque({ segments, delayMs = 45, stepDuration = 0.35, className = '', as: Tag = 'span', }: { segments: Segmento[]; delayMs?: number; stepDuration?: number; className?: string; as?: ElementType; }) { const reduced = usePrefersReducedMotion(); const [inView, setInView] = useState(false); const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || reduced) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setInView(true); observer.unobserve(el); } }, { threshold: 0.4 }, ); observer.observe(el); return () => observer.disconnect(); }, [reduced]); const words = useMemo( () => segments.flatMap((seg, si) => seg.text.split(' ').map((w, wi) => ({ w, si, wi, className: seg.className }))), [segments], ); if (reduced) { return ( {segments.map((seg, i) => ( {seg.text} {i < segments.length - 1 ? ' ' : ''} ))} ); } return ( {words.map(({ w, si, wi, className: segClass }, i) => ( // The joining space is a sibling text node, not the span's own trailing // character — a space at the very end of an inline-block's own content // collapses to zero width, which silently glued every word together. {w} {i < words.length - 1 ? ' ' : ''} ))} ); }