/* eslint-disable react/prop-types */ /** * SectionTransitions · transiciones verticales entre secciones estilo Apple. * * Monta una sola vez en cada página. Detecta las
* dentro de
y les aplica: * - Estado inicial: opacity 0 + translateY(40px) * - Cuando entran al viewport (10% visible, con offset de 120px desde abajo): * fade-in suave + slide-up a 0 * - Transición de 900ms con cubic-bezier(0.16, 1, 0.3, 1) * - Una sola vez (no animar de vuelta al hacer scroll arriba) * * Excluye: * - El Hero (primera sección) — ya tiene su animación propia de "loaded" * - El Header, Footer, Cursor (no son
dentro de
) * * Respeta prefers-reduced-motion (no-op en ese caso). */ const { useEffect: uE_St } = React; function SectionTransitions() { uE_St(() => { // No animar si el usuario prefiere movimiento reducido if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; // Encontrar secciones a animar (excluye Hero porque ya tiene su propia entrada) const hashTargetId = window.location.hash ? window.location.hash.slice(1) : null; const sections = Array.from( document.querySelectorAll('main > section[data-screen-label]') ).filter((s) => { const label = s.dataset.screenLabel || ''; // Excluir cualquier Hero (Home, Servicios, etc.) if (/hero/i.test(label)) return false; // Excluir la sección destino del hash inicial — debe quedar visible para // que scrollIntoView funcione correctamente al cargar con #anchor if (hashTargetId && s.id === hashTargetId) return false; return true; }); if (sections.length === 0) return; // Estado inicial — invisibles y desplazadas un poco abajo sections.forEach((s) => { s.style.opacity = '0'; s.style.transform = 'translate3d(0, 40px, 0)'; s.style.transition = 'opacity 900ms cubic-bezier(0.16, 1, 0.3, 1), ' + 'transform 900ms cubic-bezier(0.16, 1, 0.3, 1)'; s.style.willChange = 'opacity, transform'; }); // IntersectionObserver con offset para que la sección empiece a animar // ANTES de estar totalmente visible — efecto más fluido y anticipado const obs = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translate3d(0, 0, 0)'; // Limpiar willChange después de la animación para liberar memoria setTimeout(() => { entry.target.style.willChange = 'auto'; }, 1000); obs.unobserve(entry.target); } }); }, { threshold: 0.08, rootMargin: '0px 0px -120px 0px', } ); sections.forEach((s) => obs.observe(s)); // Cleanup return () => obs.disconnect(); }, []); return null; } window.SectionTransitions = SectionTransitions;