Files
Creeer-Web/lib/motion.ts
T
erikghandClaude Opus 5.5 89e54a6e84 feat: rediseño «El documento» en curso, contacto real y enlaces externos
Primitivas y contenido del rediseño (escala tipográfica, Revelar,
MarcaSeccion, Seccion, Antetitulo, SimboloMarca, Resplandor, ClaridadHero,
sección Proceso de ocho etapas, lib/content.ts reestructurado en seis
secciones con alias para las viejas). Mecanismo y FormaTrabajar se retiran.

Encima, lo de esta sesión:
- recorrido: creeer vuelve a responder cada situación en vez de repetirla
  (el alias apuntaba respuesta a situacion); respuestas en plural. Créditos
  de las seis fotos de México.
- contacto: WhatsApp, email (con mailto:) y LinkedIn reales; iconos de las
  redes en el cierre y el footer (IconoRed, trazos de Simple Icons, CC0).
- enlaces: lo externo abre en otra pestaña con noopener y aviso para
  lectores de pantalla (lib/enlaces.ts); mailto y anclas se quedan.
- Proceso: las ocho tarjetas no aparecían nunca en móvil (amount 0.3 sobre
  una lista de ~3000px); ahora cada una se revela sola. El ≡ de la 08 ya no
  tapa un entregable, título equilibrado, etiqueta a 12px, sin elevar al
  hover lo que no es clicable.
- Disciplinas: la frase «No son la promesa — son los medios» gana peso y
  se agrupa con título e ilustración; sin cortes huérfanos en móvil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:27:20 -06:00

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 (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) },
};
}