// ============================================================ // 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 (
Retórica
); }; const StickyContactBar = () => (
WhatsApp Brief
); const Nav = ({ currentPage = 'home' }) => { const [open, setOpen] = React.useState(false); const [scrolled, setScrolled] = React.useState(false); const links = [ { k: 'home', label: 'Inicio', href: '/' }, { k: 'services', label: 'Servicios +', href: '/servicios/' }, { k: 'portfolio', label: 'Portafolio', href: '/portafolio/' }, { k: 'studio', label: 'Estudio', href: '/estudio/' }, { k: 'journal', label: 'Contacto', href: '/contacto/' }, ]; React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 14); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); React.useEffect(() => { const close = () => setOpen(false); window.addEventListener('resize', close); return () => window.removeEventListener('resize', close); }, []); return ( <>
Contáctanos
{open && (
{links.map((l) => ( {l.label} ))} Descargar brief Contáctanos
)}
); }; const Footer = () => ( ); const PageHeader = ({ eyebrow, title, subtitle, bg = 'yellow', ctaLabel, ctaHref = '/contacto/#contact' }) => (
{eyebrow}

{title}

{subtitle &&

{subtitle}

} {ctaLabel && {ctaLabel}}
); Object.assign(window, { Preloader, Nav, Footer, PageHeader, StickyContactBar, MotionBoot });