PuntosInflexion era el único que lo montaba. Se va también la referencia que le quedaba colgando en el comentario de useAutoStep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
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<SVGSVGElement>(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 <motion.*> 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.
|
|
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) },
|
|
};
|
|
}
|