Cada respuesta llega cuando su par alcanza el centro de la pantalla: la regla dorada se dibuja y el bloque entra. El estado solo avanza, así que nada se apaga por detrás del lector. El scroll no es el único mando — hover y foco de teclado llevan al mismo sitio. Corrige un fallo que las pruebas no podían ver: Framer resuelve la variante escribiendo `opacity: 1` en el style inline, que gana a la clase opacity-0 de Tailwind. El revelado no ocurría; solo se animaba la regla, que es un transform que Framer no toca. La entrada y el revelado viven ahora en elementos distintos, con una prueba que lo fija. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
3.5 KiB
TypeScript
66 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Container } from '@/components/primitives/Container';
|
|
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
|
|
import { ParDialogo } from '@/components/primitives/ParDialogo';
|
|
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
|
|
import { puntosInflexion } from '@/lib/content';
|
|
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
|
|
|
|
export function PuntosInflexion() {
|
|
const reduced = usePrefersReducedMotion();
|
|
|
|
// How far down the conversation the reader has got. It only ever moves
|
|
// forward: an answer that faded back out on the way past would leave the page
|
|
// emptier the further you read. Scroll drives it, and so do hover and
|
|
// keyboard focus, so the reader can also run the conversation themselves
|
|
// instead of only being carried through it.
|
|
const [alcanzado, setAlcanzado] = useState(0);
|
|
const activar = useCallback((n: number) => setAlcanzado((previo) => Math.max(previo, n)), []);
|
|
|
|
return (
|
|
<section className="bg-gris-claro relative overflow-hidden">
|
|
<FondoPuntos className="-bottom-6 -right-12 w-80 md:w-[32rem] text-azul-profundo/10" distance={32} />
|
|
<Container className="relative py-16 md:py-24">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
|
|
<div>
|
|
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
|
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.eyebrow}</motion.p>
|
|
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
|
className="mt-4 font-sans font-black text-seccion text-azul-profundo max-w-3xl">{puntosInflexion.title}</motion.h2>
|
|
</div>
|
|
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
|
className="mt-8 md:mt-0 shrink-0">
|
|
<IlustracionInflexion className="w-56 md:w-72 h-auto mx-auto" />
|
|
</motion.div>
|
|
</div>
|
|
|
|
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
|
className="mt-14 text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.lead}</motion.p>
|
|
|
|
{/* The conversation. One <li> per exchange, read top to bottom — no
|
|
carousel at any breakpoint, because "te bajas" is the whole shape
|
|
the dueña asked for. Each answer arrives when the reader reaches its
|
|
pair, so the section answers as you go down instead of sitting there
|
|
already answered. The stagger reaches ParDialogo's two halves
|
|
through React context. */}
|
|
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
|
className="mt-8 list-none p-0 flex flex-col gap-16 md:gap-24">
|
|
{puntosInflexion.items.map((item, i) => (
|
|
<ParDialogo
|
|
key={item.name}
|
|
n={i + 1}
|
|
{...item}
|
|
activo={i + 1 <= alcanzado}
|
|
reducido={reduced}
|
|
onActivar={activar}
|
|
/>
|
|
))}
|
|
</motion.ul>
|
|
</Container>
|
|
</section>
|
|
);
|
|
}
|