Files
Creeer-Web/components/primitives/EscenaDialogo.tsx
T
erikghandClaude Opus 5 379d80b915 fix(recorrido): no se puede quedar entre dos estados
Las tarjetas iban atadas a la posición del scroll, así que cualquier
punto intermedio era un fotograma válido: parando a media rueda, la
tarjeta de creeer se quedaba como una losa gris translúcida y parecía un
fallo de render.

Ahora el scroll solo elige el tiempo —doce: pregunta, respuesta,
pregunta…— y las tarjetas animan hacia ese tiempo por duración. Pares
donde pares, aterrizan. Verificado en 41 paradas repartidas por la
sección: cero opacidades a medias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:58:08 -06:00

104 lines
3.8 KiB
TypeScript

'use client';
import { motion, useTransform, type MotionValue } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
// One beat of the pinned conversation, staged as two calling cards laid on a
// table: the reader's, in white, and creeer's, in navy, handed back.
//
// The cards are driven by STATE, not by scroll position. Mapping opacity
// straight onto scroll made every intermediate position a valid frame, so
// stopping mid-swipe left creeer's card as a half-transparent grey rectangle —
// it read as a rendering fault. Scroll now only decides which beat we are on;
// the cards animate to that beat over time and always land somewhere finished,
// wherever the reader lets go.
//
// Every scene stays mounted and stacked in the same place. Mounting only the
// active one would animate more cheaply and would strip five sixths of the
// section out of the static export.
const EASE = [0.22, 1, 0.36, 1] as const;
export function EscenaDialogo({
i,
total,
progreso,
activa,
respondida,
n,
name,
situacion,
respuesta,
}: {
i: number;
total: number;
progreso: MotionValue<number>;
activa: boolean;
respondida: boolean;
n: string;
name: string;
situacion: string;
respuesta: string;
}) {
const paso = 1 / total;
// The one thing still tied to scroll. A drifting numeral never looks broken
// at any position it can be caught in, so it costs nothing and keeps the
// stage from feeling frozen between beats.
const cifraY = useTransform(progreso, [i * paso - paso, (i + 1) * paso + paso], [60, -60]);
return (
<motion.li
data-activa={activa ? 'true' : 'false'}
animate={{ opacity: activa ? 1 : 0, y: activa ? 0 : 26 }}
transition={{ duration: 0.45, ease: EASE }}
className="absolute inset-x-0 top-1/2 -translate-y-1/2"
style={{ pointerEvents: activa ? 'auto' : 'none' }}
>
<motion.span
aria-hidden="true"
style={{ y: cifraY }}
className="pointer-events-none absolute -top-32 right-0 select-none font-sans font-black leading-none text-azul-profundo/[0.045] text-[9rem] md:text-[17rem]"
>
{n}
</motion.span>
{/* The reader's card. It gives way a little once the answer lands: still
legible, no longer the thing you are looking at. */}
<motion.div
animate={{
opacity: respondida ? 0.72 : 1,
rotate: respondida ? -1.8 : -0.6,
y: respondida ? -14 : 0,
}}
transition={{ duration: 0.5, ease: EASE }}
className="relative z-10 max-w-xl rounded-xl border border-azul-profundo/10 bg-white p-7 shadow-[0_28px_70px_-32px_rgba(10,29,61,0.5)] md:p-9"
>
<p className="text-[0.65rem] font-black uppercase tracking-[0.34em] text-dorado">
{n} · {name}
</p>
<p className="mt-4 font-sans font-black leading-[1.12] text-azul-profundo text-2xl md:text-4xl">
«{situacion}»
</p>
</motion.div>
{/* creeer's card, handed back. Navy against the reader's white, tilted the
other way, overlapping the corner — two cards on a table, not two
paragraphs in a column. */}
<motion.div
animate={{
opacity: respondida ? 1 : 0,
y: respondida ? 0 : 70,
scale: respondida ? 1 : 0.94,
rotate: respondida ? 1.6 : 5.5,
}}
transition={{ duration: 0.55, ease: EASE }}
className="relative z-20 -mt-5 ml-auto max-w-lg rounded-xl bg-azul-profundo p-7 shadow-[0_34px_80px_-30px_rgba(10,29,61,0.75)] md:-mt-8 md:mr-6 md:p-9"
>
<span aria-hidden="true" className="absolute left-0 top-7 h-10 w-[3px] rounded-r bg-dorado md:top-9" />
<Wordmark size="sm" onDark />
<p className="mt-4 text-base leading-relaxed text-white/75 md:text-xl">{respuesta}</p>
</motion.div>
</motion.li>
);
}