{/* Hero visual: the particle field sits straight on the card, one color throughout. */}
{
const { x, y } = local(e);
Object.assign(sim.current.pointer, { x, y, sx: x, sy: y });
}}
onPointerMove={(e) =>
Object.assign(sim.current.pointer, local(e), { active: true })
}
onPointerLeave={() => (sim.current.pointer.active = false)}
onPointerDown={(e) => {
// A droplet: a ripple ring spreads out from the press.
if (still) return;
const s = sim.current;
s.ripples.push({ ...local(e), t: s.time });
}}
>
{title}
{description && (
{description}
)}
(launch(), onOpen?.()),
})}
className="group mt-5 flex h-11 w-full cursor-pointer items-center justify-center gap-1.5 rounded-full text-sm font-medium outline-none transition-[background-color,transform] duration-200 bg-[var(--ghost)] hover:bg-[var(--ghost-hover)] focus-visible:ring-2 focus-visible:ring-zinc-400 active:scale-[0.98] active:bg-[var(--ghost-press)]"
style={
{
color: textColor,
"--ghost": `color-mix(in srgb, ${textColor} 6%, transparent)`,
"--ghost-hover": `color-mix(in srgb, ${textColor} 9%, transparent)`,
"--ghost-press": `color-mix(in srgb, ${textColor} 13%, transparent)`,
} as React.CSSProperties
}
>
{cta}
{/* Hover: the arrow slides out to the right while a fresh one slides in from the left. */}
{/* Film grain over the whole card, particles included. */}
{grain > 0 && (
)}
);
},
);