import { useEffect, useRef, useState } from 'react'; import type { Variants } from 'framer-motion'; // EaseOutExpo — smooth editorial deceleration export const easeOutExpo = [0.22, 1, 0.36, 1] as const; export const fadeInUp: Variants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: easeOutExpo } }, }; export const staggerContainer: Variants = { hidden: {}, visible: { transition: { staggerChildren: 0.1 } }, }; export const viewportOnce = { once: true, amount: 0.3 } as const; // Hook: returns true if user prefers reduced motion. Components use this to // disable Framer Motion animations (per spec, a11y requirement). export function usePrefersReducedMotion() { const [reduced, setReduced] = useState(false); useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); setReduced(mq.matches); const handler = () => setReduced(mq.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); return reduced; } // Shared "draw once when scrolled into view" wiring for the SVG illustrations. // Returns the ref to attach, whether the element has been seen, and the // reduced-motion flag the caller needs anyway to pick its render branch. // Observes nothing under reduced motion — those branches render statically. export function useEnVista(umbral = 0.4) { const reduced = usePrefersReducedMotion(); const ref = useRef(null); const [enVista, setEnVista] = useState(false); useEffect(() => { const el = ref.current; if (!el || reduced) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setEnVista(true); observer.unobserve(el); } }, { threshold: umbral }, ); observer.observe(el); return () => observer.disconnect(); }, [reduced, umbral]); return { ref, enVista, reduced }; } // Pass to as motion-reduced guard. When reduced, variants resolve to visible immediately. export const motionProps = (reduced: boolean) => reduced ? { initial: 'visible' as const, animate: 'visible' as const, whileInView: undefined, viewport: undefined } : {}; // Cycles through `count` steps on its own (Mecanismo, ComoEmpezar): each // manual change restarts the dwell timer, hovering pauses it, and it never // advances at all for reduced-motion users — same shape as the Carrusel // autoplay, for step indicators that aren't a carousel. export function useAutoStep(count: number, intervalMs = 3500) { const reduced = usePrefersReducedMotion(); const [active, setActive] = useState(0); const [paused, setPaused] = useState(false); useEffect(() => { if (reduced || paused || count <= 1) return; const id = setTimeout(() => setActive((a) => (a + 1) % count), intervalMs); return () => clearTimeout(id); }, [active, reduced, paused, count, intervalMs]); return { active, setActive, hoverProps: { onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false) }, }; }