feat(primitivas): capa gráfica y de movimiento reutilizable
Diez primitivas que dan a la landing el "gráfico y movimiento" que pidió la dueña, todas con guarda de prefers-reduced-motion: - FondoPuntos / PatronPuntos — el patrón de puntos del manual - TarjetaSpotlight, TextoDesenfoque, ContadorNumero — realce y reveal - Carrusel — deslizable, con contador, autoplay pausable - Icono, SimboloTresLineas, IlustracionInflexion — línea, sin relleno - FondoFoto — duotono azul/dorado sobre foto useAutoStep en lib/motion.ts comparte la forma del autoplay del carrusel para indicadores de paso que no son carrusel. IlustracionInflexion documenta un detalle que muerde: motion.path aplica su propio andamiaje de dasharray aunque no reciba initial/animate, así que la rama de reduced motion renderiza <path> planos en vez de intentar neutralizar los animados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9758a92f9
commit
5b400cb161
19 files changed
+881
No files matched your search
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { animate } from 'framer-motion';
|
||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
||||
|
||||
// Counts up to `hasta` once scrolled into view. Only ever used with a number
|
||||
// that's already written in the approved copy (e.g. "30+ años") — a visual
|
||||
// echo of real content, never a stand-in for real metrics we don't have yet.
|
||||
export function ContadorNumero({ hasta, sufijo = '', className = '' }: { hasta: number; sufijo?: string; className?: string }) {
|
||||
const reduced = usePrefersReducedMotion();
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [display, setDisplay] = useState(reduced ? hasta : 0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || reduced) return;
|
||||
let controls: { stop: () => void } | undefined;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(el);
|
||||
controls = animate(0, hasta, {
|
||||
duration: 1.4,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
onUpdate: (v) => setDisplay(Math.round(v)),
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
controls?.stop();
|
||||
};
|
||||
}, [reduced, hasta]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className={className}>
|
||||
{display}
|
||||
{sufijo}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user