// ============================================================ // ESTUDIO RETÓRICA · Navegación, motion y chrome compartido // ============================================================ const MotionBoot = () => { React.useEffect(() => { const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const revealNodes = Array.from(document.querySelectorAll('[data-reveal]')); const counterNodes = Array.from(document.querySelectorAll('[data-counter]')); revealNodes.forEach((node, index) => { node.style.setProperty('--reveal-delay', `${Math.min(index * 60, 360)}ms`); }); let revealObserver; if (!reduceMotion && revealNodes.length) { revealObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-revealed'); revealObserver.unobserve(entry.target); } }); }, { threshold: 0.18, rootMargin: '0px 0px -8% 0px' }); revealNodes.forEach((node) => revealObserver.observe(node)); } else { revealNodes.forEach((node) => node.classList.add('is-revealed')); } let counterObserver; if (counterNodes.length) { const animateCounter = (node) => { if (node.dataset.counted === 'true') return; node.dataset.counted = 'true'; const target = Number(node.dataset.counter || '0'); const duration = reduceMotion ? 0 : 1200; const start = performance.now(); const step = (time) => { const progress = duration === 0 ? 1 : Math.min((time - start) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); node.textContent = String(Math.round(target * eased)); if (progress < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); }; counterObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { animateCounter(entry.target); counterObserver.unobserve(entry.target); } }); }, { threshold: 0.35 }); counterNodes.forEach((node) => counterObserver.observe(node)); } return () => { if (revealObserver) revealObserver.disconnect(); if (counterObserver) counterObserver.disconnect(); }; }, []); return null; }; const ScrollProgress = () => { const [progress, setProgress] = React.useState(0); React.useEffect(() => { const onScroll = () => { const doc = document.documentElement; const max = doc.scrollHeight - window.innerHeight; const value = max > 0 ? (window.scrollY / max) * 100 : 0; setProgress(Math.min(100, Math.max(0, value))); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, []); return (
); }; const Preloader = () => { const [hidden, setHidden] = React.useState(false); React.useEffect(() => { const t = setTimeout(() => setHidden(true), 420); return () => clearTimeout(t); }, []); return (