"use client"; import { motion, useInView, useReducedMotion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type CardStackHue = "sky" | "emerald" | "amber" | "violet" | "rose" | "orange" | "indigo" | "teal"; const hues: Record = { sky: "from-sky-400 via-sky-500 to-blue-700", emerald: "from-emerald-400 via-emerald-500 to-teal-700", amber: "from-amber-300 via-amber-500 to-orange-600", violet: "from-violet-400 via-violet-500 to-indigo-700", rose: "from-rose-400 via-rose-500 to-pink-700", orange: "from-orange-400 via-orange-500 to-red-600", indigo: "from-indigo-400 via-indigo-600 to-violet-900", teal: "from-teal-300 via-teal-500 to-cyan-700", }; export interface PaymentCard { issuer: string; type?: string; last4: string; holder: string; expires: string; hue?: CardStackHue; cvc?: string; } export interface CardStackProps { cards: PaymentCard[]; autoPlay?: boolean; className?: string; } const spring = { type: "spring", stiffness: 260, damping: 26 } as const; const flipSpring = { type: "spring", stiffness: 180, damping: 22 } as const; function Contactless({ className }: { className?: string }) { return ( ); } function NetworkMark({ className }: { className?: string }) { return ( ); } function Chip() { return (
); } function Front({ card }: { card: PaymentCard }) { return (
{card.issuer} {card.type && {card.type}}
•••• •••• •••• {card.last4}
Card holder {card.holder} Expires {card.expires}
); } function Back({ card }: { card: PaymentCard }) { return (
{card.cvc ?? "000"}

Issued by {card.issuer}

); } function Surface({ hue, children }: { hue: CardStackHue; children: React.ReactNode }) { return (
{children}
); } function placement(rank: number, count: number, fanned: boolean, lifted: boolean) { if (rank === 0 && lifted) return { x: "0%", y: "-74%", rotate: 0, scale: 1 }; if (!fanned) return { x: "0%", y: `${-rank * 23}%`, rotate: 0, scale: 1 - rank * 0.03 }; if (rank === 0) return { x: "0%", y: "-46%", rotate: 0, scale: 1 }; const side = count === 2 ? 1 : rank === 1 ? -1 : 1; return { x: `${side * 30}%`, y: "-38%", rotate: side * 10, scale: 0.94 }; } function Wallet() { return (
); } function useCalmMotion() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export function CardStack({ cards, autoPlay = true, className }: CardStackProps) { const reduce = useCalmMotion(); const root = useRef(null); const inView = useInView(root); const list = cards.slice(0, 3); const [order, setOrder] = useState(() => list.map((_, i) => i)); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); const [pinned, setPinned] = useState(false); const [flipped, setFlipped] = useState(false); const [auto, setAuto] = useState({ fan: false, flip: false }); const [touched, setTouched] = useState(false); const user = hovered || focused || pinned || touched; const fanned = user ? hovered || focused || pinned : auto.fan; const showBack = user ? flipped : auto.flip; useEffect(() => { if (!autoPlay || user || reduce || !inView) { setAuto({ fan: false, flip: false }); return; } const steps: [number, { fan: boolean; flip: boolean }][] = [ [1600, { fan: true, flip: false }], [1100, { fan: true, flip: true }], [2600, { fan: true, flip: false }], [1000, { fan: false, flip: false }], ]; let i = 0; let t: ReturnType; const next = () => { const [ms, state] = steps[i % steps.length]; t = setTimeout(() => { setAuto(state); i++; next(); }, i % steps.length === 0 && i > 0 ? ms + 2400 : ms); }; next(); return () => clearTimeout(t); }, [autoPlay, user, reduce, inView]); return (
!e.currentTarget.contains(e.relatedTarget) && setFocused(false)} onFocus={(e) => e.target.matches(":focus-visible") && setFocused(true)} onPointerEnter={(e) => e.pointerType === "mouse" && setHovered(true)} onPointerLeave={(e) => { if (e.pointerType !== "mouse") return; setHovered(false); setFlipped(false); }} role="group" > {list.map((card, i) => { const rank = order.indexOf(i); const isFront = rank === 0; const place = placement(rank, list.length, fanned, isFront && showBack); const hue = card.hue ?? (["violet", "emerald", "orange"] as const)[i]; return ( { setTouched(true); if (!fanned && !hovered) { setPinned(true); return; } if (isFront) setFlipped((f) => !f); else { setFlipped(false); setOrder((o) => [i, ...o.filter((x) => x !== i)]); } }} transition={reduce ? { duration: 0 } : flipSpring} type="button" whileTap={{ scale: 0.98 }} >
); })}
); }