"use client";
import * as React from "react";
import { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react";
import { ArrowRight, Bookmark, Check, ChevronDown, Gift, Lock, Minus, Plus, ShoppingCart, Tag, Trash2, Truck, Undo2, X, PartyPopper } from "lucide-react";
import { cn } from "@/lib/utils";
import { ProductArt, type ArtKind } from "./product-art";
export type CartItem = {
id: string;
name: string;
variant?: string;
price: number;
compareAt?: number;
qty: number;
kind: ArtKind;
hex: string;
accent?: string;
label?: string;
maxQty?: number;
};
export type PromoRule = { code: string; type: "percent" | "fixed" | "shipping"; value: number; label: string; minSubtotal?: number };
export type ShippingZone = { id: string; label: string; cost: number; eta: string };
export type CheckoutPayload = { items: CartItem[]; subtotal: number; discount: number; shipping: number; tax: number; total: number; promo?: string; giftNote?: string };
export interface CartPageProps {
initialItems?: CartItem[];
initialSaved?: CartItem[];
recommendations?: CartItem[];
promos?: PromoRule[];
zones?: ShippingZone[];
freeShippingAt?: number;
/** Included VAT rate used for the tax line (0.23 = 23%). */
taxRate?: number;
currency?: string;
locale?: string;
onCheckout?: (payload: CheckoutPayload) => void;
onChange?: (items: CartItem[]) => void;
className?: string;
}
const I = (id: string, name: string, variant: string, price: number, qty: number, kind: ArtKind, hex: string, accent?: string, extra: Partial<CartItem> = {}): CartItem => ({ id, name, variant, price, qty, kind, hex, accent, ...extra });
const ITEMS: CartItem[] = [
I("c1", "Highland Single Malt 12", "Sherry cask · 70 cl", 39.9, 1, "bottle", "#b45309", "#f5efe0", { compareAt: 49.9, label: "Lumen" }),
I("c2", "Reserva Tinto 2019", "75 cl · Case of 2", 18.9, 2, "wine", "#7f1d1d", "#d4af37", { label: "Orbit" }),
I("c3", "Crystal Rocks Tumbler", "Set of 2", 12, 1, "tumbler", "#d97706", "#ffffff"),
];
const SAVED: CartItem[] = [I("s1", "Hazy IPA 4-pack", "4 × 440 ml", 11.9, 1, "can", "#ca8a04", "#1e293b", { label: "Brewlab" })];
const RECS: CartItem[] = [
I("r1", "Whisky Stones Set", "9 pcs", 19, 1, "box", "#57534e", "#a8a29e"),
I("r2", "Champagne Brut NV", "75 cl", 42, 1, "wine", "#ca8a04", "#111827", { label: "Orbit" }),
I("r3", "Soy Wax Candle", "Oak & amber", 16, 1, "candle", "#e7d5c0", "#1c1917", { label: "Lumen" }),
I("r4", "Coastal Dry Gin", "70 cl", 29.5, 1, "bottle", "#0ea5e9", "#f0f9ff", { label: "Northwind" }),
I("r5", "Barrel-aged Stout", "440 ml", 6.5, 1, "can", "#292524", "#fbbf24", { label: "Brewlab" }),
];
const PROMOS: PromoRule[] = [
{ code: "SAVE10", type: "percent", value: 10, label: "10% off your order" },
{ code: "WELCOME5", type: "fixed", value: 5, label: "€5 welcome discount", minSubtotal: 30 },
{ code: "FREESHIP", type: "shipping", value: 0, label: "Free shipping" },
];
const ZONES: ShippingZone[] = [
{ id: "std", label: "Standard (2–3 days)", cost: 4.99, eta: "Fri, 27 Sep" },
{ id: "exp", label: "Express (next day)", cost: 9.9, eta: "Tomorrow" },
{ id: "pick", label: "Click & collect", cost: 0, eta: "Ready in 2 hours" },
];
const ring = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
function AnimatedMoney({ value, fmt, className }: { value: number; fmt: Intl.NumberFormat; className?: string }) {
const reduce = useReducedMotion();
const mv = useMotionValue(value);
const text = useTransform(mv, (v) => fmt.format(v));
React.useEffect(() => {
if (reduce) {
mv.set(value);
return;
}
const c = animate(mv, value, { duration: 0.5, ease: [0.22, 1, 0.36, 1] });
return () => c.stop();
}, [value, mv, reduce]);
return <motion.span className={cn("tabular-nums", className)}>{text}</motion.span>;
}
export function CartPage({
initialItems = ITEMS,
initialSaved = SAVED,
recommendations = RECS,
promos = PROMOS,
zones = ZONES,
freeShippingAt = 100,
taxRate = 0.23,
currency = "EUR",
locale = "en-IE",
onCheckout,
onChange,
className,
}: CartPageProps) {
const reduce = useReducedMotion();
const fmt = React.useMemo(() => new Intl.NumberFormat(locale, { style: "currency", currency }), [locale, currency]);
const [items, setItems] = React.useState(initialItems);
const [saved, setSaved] = React.useState(initialSaved);
const [undo, setUndo] = React.useState<{ item: CartItem; index: number } | null>(null);
const [code, setCode] = React.useState("");
const [promo, setPromo] = React.useState<PromoRule | null>(null);
const [promoMsg, setPromoMsg] = React.useState<{ ok: boolean; text: string } | null>(null);
const [zone, setZone] = React.useState(zones[0]?.id);
const [gift, setGift] = React.useState(false);
const [note, setNote] = React.useState("");
const [busy, setBusy] = React.useState(false);
const [recAdded, setRecAdded] = React.useState<string | null>(null);
React.useEffect(() => onChange?.(items), [items, onChange]);
React.useEffect(() => {
if (!undo) return;
const t = window.setTimeout(() => setUndo(null), 6000);
return () => window.clearTimeout(t);
}, [undo]);
const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
const savings = items.reduce((s, i) => s + (i.compareAt ? (i.compareAt - i.price) * i.qty : 0), 0);
const promoValid = promo && (!promo.minSubtotal || subtotal >= promo.minSubtotal);
const discount = promoValid ? (promo.type === "percent" ? Math.round(subtotal * promo.value) / 100 : promo.type === "fixed" ? Math.min(promo.value, subtotal) : 0) : 0;
const z = zones.find((x) => x.id === zone) ?? zones[0];
const freeShip = subtotal - discount >= freeShippingAt || (promoValid && promo.type === "shipping");
const shipping = items.length === 0 ? 0 : freeShip && z.id === zones[0]?.id ? 0 : freeShip ? Math.max(0, z.cost - zones[0].cost) : z.cost;
const total = Math.max(0, subtotal - discount + shipping);
const tax = total - total / (1 + taxRate);
const progress = Math.min(1, (subtotal - discount) / freeShippingAt);
const count = items.reduce((s, i) => s + i.qty, 0);
const setQty = (id: string, q: number) => setItems((l) => l.map((i) => (i.id === id ? { ...i, qty: Math.max(1, Math.min(i.maxQty ?? 12, q)) } : i)));
const remove = (id: string) => {
const index = items.findIndex((i) => i.id === id);
if (index < 0) return;
setUndo({ item: items[index], index });
setItems((l) => l.filter((i) => i.id !== id));
};
const restore = () => {
if (!undo) return;
setItems((l) => [...l.slice(0, undo.index), undo.item, ...l.slice(undo.index)]);
setUndo(null);
};
const saveForLater = (id: string) => {
const it = items.find((i) => i.id === id);
if (!it) return;
setItems((l) => l.filter((i) => i.id !== id));
setSaved((s) => [it, ...s]);
};
const moveToCart = (id: string) => {
const it = saved.find((i) => i.id === id);
if (!it) return;
setSaved((s) => s.filter((i) => i.id !== id));
setItems((l) => [...l, it]);
};
const addRec = (r: CartItem) => {
setItems((l) => (l.some((i) => i.id === r.id) ? l.map((i) => (i.id === r.id ? { ...i, qty: i.qty + 1 } : i)) : [...l, { ...r, qty: 1 }]));
setRecAdded(r.id);
window.setTimeout(() => setRecAdded((a) => (a === r.id ? null : a)), 1300);
};
const applyPromo = (e: React.FormEvent) => {
e.preventDefault();
const rule = promos.find((p) => p.code === code.trim().toUpperCase());
if (!rule) {
setPromoMsg({ ok: false, text: "That code isn't valid. Try SAVE10." });
return;
}
if (rule.minSubtotal && subtotal < rule.minSubtotal) {
setPromoMsg({ ok: false, text: `Spend ${fmt.format(rule.minSubtotal)} to use ${rule.code}.` });
return;
}
setPromo(rule);
setCode("");
setPromoMsg({ ok: true, text: `${rule.label} applied` });
};
const checkout = () => {
if (!items.length) return;
setBusy(true);
window.setTimeout(() => {
setBusy(false);
onCheckout?.({ items, subtotal, discount, shipping, tax, total, promo: promoValid ? promo.code : undefined, giftNote: gift ? note : undefined });
}, 1200);
};
return (
<section className={cn("w-full bg-background py-8 text-foreground sm:py-12", className)}>
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<div className="flex items-baseline justify-between gap-4">
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">Your cart</h2>
<p className="text-sm text-muted-foreground" aria-live="polite">
{count} {count === 1 ? "item" : "items"}
</p>
</div>
<div className="mt-6 grid gap-8 lg:grid-cols-[1fr_380px] lg:gap-10">
<div className="min-w-0">
{/* Free-shipping meter */}
<div className="rounded-2xl border bg-card p-4">
<p className="flex items-center gap-2 text-sm" aria-live="polite">
{freeShip ? (
<>
<PartyPopper className="size-4 text-emerald-500" aria-hidden />
<span>
<span className="font-semibold">You've unlocked free standard shipping!</span>
</span>
</>
) : (
<>
<Truck className="size-4 text-primary" aria-hidden />
<span>
Add <span className="font-semibold tabular-nums">{fmt.format(Math.max(0, freeShippingAt - subtotal + discount))}</span> more for free shipping
</span>
</>
)}
</p>
<div className="relative mt-3 h-2 overflow-hidden rounded-full bg-muted" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round((freeShip ? 1 : progress) * 100)} aria-label="Progress to free shipping">
<motion.div
className={cn("absolute inset-y-0 left-0 rounded-full", freeShip ? "bg-emerald-500" : "bg-gradient-to-r from-primary/70 to-primary")}
animate={{ width: `${(freeShip ? 1 : progress) * 100}%` }}
transition={{ type: "spring", stiffness: 120, damping: 20 }}
/>
</div>
</div>
{/* Lines */}
<LayoutGroup>
<ul className="mt-4 divide-y rounded-2xl border bg-card">
<AnimatePresence initial={false} mode="popLayout">
{items.map((it) => (
<motion.li
key={it.id}
layout={!reduce}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, x: reduce ? 0 : -40, transition: { duration: 0.2 } }}
className="overflow-hidden"
>
<div className="flex gap-4 p-4 sm:p-5">
<div className="relative size-20 shrink-0 overflow-hidden rounded-xl bg-muted/70 sm:size-24">
<div className="absolute inset-1.5">
<ProductArt kind={it.kind} color={it.hex} accent={it.accent} label={it.label} />
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="truncate text-sm font-semibold sm:text-base">{it.name}</h3>
{it.variant && <p className="text-xs text-muted-foreground sm:text-sm">{it.variant}</p>}
<p className="mt-1 text-xs text-muted-foreground tabular-nums">
{fmt.format(it.price)} each
{it.compareAt && <s className="ml-1.5">{fmt.format(it.compareAt)}</s>}
</p>
</div>
<div className="text-right">
<AnimatedMoney value={it.price * it.qty} fmt={fmt} className="text-sm font-semibold sm:text-base" />
{it.compareAt && <p className="whitespace-nowrap text-[11px] font-medium text-emerald-600 dark:text-emerald-400">Save {fmt.format((it.compareAt - it.price) * it.qty)}</p>}
</div>
</div>
<div className="mt-auto flex flex-wrap items-center gap-x-4 gap-y-2 pt-3">
<div className="flex h-9 items-center rounded-lg border" role="group" aria-label={`Quantity for ${it.name}`}>
<button type="button" aria-label="Decrease" disabled={it.qty <= 1} onClick={() => setQty(it.id, it.qty - 1)} className={cn("grid h-full w-8 place-items-center rounded-l-lg hover:bg-muted disabled:opacity-40", ring)}>
<Minus className="size-3.5" />
</button>
<label className="sr-only" htmlFor={`qty-${it.id}`}>
Quantity
</label>
<input
id={`qty-${it.id}`}
inputMode="numeric"
value={it.qty}
onChange={(e) => {
const n = parseInt(e.target.value.replace(/\D/g, ""), 10);
if (!Number.isNaN(n)) setQty(it.id, n);
}}
className="h-full w-9 bg-transparent text-center text-sm font-semibold tabular-nums focus:outline-none"
/>
<button type="button" aria-label="Increase" disabled={it.qty >= (it.maxQty ?? 12)} onClick={() => setQty(it.id, it.qty + 1)} className={cn("grid h-full w-8 place-items-center rounded-r-lg hover:bg-muted disabled:opacity-40", ring)}>
<Plus className="size-3.5" />
</button>
</div>
<button type="button" onClick={() => saveForLater(it.id)} className={cn("flex items-center gap-1.5 rounded text-xs font-medium text-muted-foreground hover:text-foreground", ring)}>
<Bookmark className="size-3.5" aria-hidden /> Save for later
</button>
<button type="button" onClick={() => remove(it.id)} className={cn("flex items-center gap-1.5 rounded text-xs font-medium text-muted-foreground hover:text-destructive", ring)} aria-label={`Remove ${it.name}`}>
<Trash2 className="size-3.5" aria-hidden /> Remove
</button>
</div>
</div>
</div>
</motion.li>
))}
</AnimatePresence>
{items.length === 0 && (
<li className="flex flex-col items-center px-6 py-14 text-center">
<motion.span initial={{ scale: 0.6, rotate: -10 }} animate={{ scale: 1, rotate: 0 }} className="grid size-14 place-items-center rounded-2xl bg-muted">
<ShoppingCart className="size-6 text-muted-foreground" aria-hidden />
</motion.span>
<p className="mt-4 font-semibold">Your cart is empty</p>
<p className="mt-1 text-sm text-muted-foreground">Pick something from the suggestions below.</p>
</li>
)}
</ul>
<AnimatePresence>
{undo && (
<motion.div
role="status"
initial={{ opacity: 0, y: -8, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="mt-3 flex items-center gap-3 rounded-xl bg-foreground px-4 py-3 text-sm text-background">
<Trash2 className="size-4 shrink-0 opacity-70" aria-hidden />
<span className="min-w-0 flex-1 truncate">
Removed <span className="font-medium">{undo.item.name}</span>
</span>
<button type="button" onClick={restore} className={cn("flex items-center gap-1.5 rounded-md px-2 py-1 font-semibold hover:bg-background/15", ring)}>
<Undo2 className="size-4" aria-hidden /> Undo
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Saved */}
{saved.length > 0 && (
<motion.div layout={!reduce} className="mt-8">
<h3 className="text-sm font-semibold">Saved for later ({saved.length})</h3>
<ul className="mt-3 grid gap-3 sm:grid-cols-2">
<AnimatePresence initial={false} mode="popLayout">
{saved.map((it) => (
<motion.li key={it.id} layout={!reduce} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} className="flex items-center gap-3 rounded-2xl border border-dashed p-3">
<span className="size-14 shrink-0 rounded-lg bg-muted/70 p-1">
<ProductArt kind={it.kind} color={it.hex} accent={it.accent} label={it.label} />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{it.name}</p>
<p className="text-xs text-muted-foreground tabular-nums">{fmt.format(it.price)}</p>
</div>
<button type="button" onClick={() => moveToCart(it.id)} className={cn("h-8 shrink-0 rounded-lg border px-3 text-xs font-medium hover:bg-muted", ring)}>
Move to cart
</button>
<button type="button" onClick={() => setSaved((s) => s.filter((x) => x.id !== it.id))} aria-label={`Delete ${it.name} from saved`} className={cn("grid size-8 shrink-0 place-items-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground", ring)}>
<X className="size-4" />
</button>
</motion.li>
))}
</AnimatePresence>
</ul>
</motion.div>
)}
{/* Recs */}
<motion.div layout={!reduce} className="mt-10">
<div className="flex items-baseline justify-between">
<h3 className="text-sm font-semibold">Complete your order</h3>
<span className="text-xs text-muted-foreground">Swipe for more</span>
</div>
<ul className="-mx-4 mt-3 flex snap-x snap-mandatory scroll-px-4 gap-3 overflow-x-auto px-4 pb-2 [scrollbar-width:none] sm:mx-0 sm:scroll-px-0 sm:px-0">
{recommendations.map((r) => (
<li key={r.id} className="w-40 shrink-0 snap-start">
<div className="group relative aspect-square overflow-hidden rounded-2xl bg-muted/70">
<div className="absolute inset-3 transition duration-500 group-hover:scale-105">
<ProductArt kind={r.kind} color={r.hex} accent={r.accent} label={r.label} />
</div>
<button
type="button"
onClick={() => addRec(r)}
aria-label={`Add ${r.name} to cart`}
className={cn("absolute bottom-2 right-2 grid size-9 place-items-center rounded-full shadow-lg transition", recAdded === r.id ? "bg-emerald-600 text-white" : "bg-background text-foreground hover:scale-110", ring)}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span key={String(recAdded === r.id)} initial={{ scale: 0, rotate: -90 }} animate={{ scale: 1, rotate: 0 }} exit={{ scale: 0 }}>
{recAdded === r.id ? <Check className="size-4" /> : <Plus className="size-4" />}
</motion.span>
</AnimatePresence>
</button>
</div>
<p className="mt-2 truncate text-sm font-medium">{r.name}</p>
<p className="text-xs text-muted-foreground tabular-nums">{fmt.format(r.price)}</p>
</li>
))}
</ul>
</motion.div>
</LayoutGroup>
</div>
{/* Summary */}
<aside className="lg:sticky lg:top-6 lg:self-start" aria-label="Order summary">
<div className="rounded-3xl border bg-card p-5 shadow-sm sm:p-6">
<h3 className="text-lg font-semibold">Order summary</h3>
<form onSubmit={applyPromo} className="mt-4" noValidate>
<label htmlFor="cart-promo" className="text-sm font-medium">
Promo code
</label>
<div className="mt-1.5 flex gap-2">
<div className="relative min-w-0 flex-1">
<Tag className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
<input
id="cart-promo"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="SAVE10"
aria-describedby="cart-promo-msg"
className={cn("h-10 w-full rounded-lg border bg-background pl-9 pr-3 text-sm uppercase placeholder:normal-case", ring)}
/>
</div>
<button disabled={!code.trim()} className={cn("h-10 rounded-lg bg-secondary px-4 text-sm font-medium text-secondary-foreground transition hover:bg-accent disabled:opacity-50", ring)}>Apply</button>
</div>
<div id="cart-promo-msg" aria-live="polite">
<AnimatePresence mode="wait">
{promoMsg && (
<motion.p key={promoMsg.text} initial={{ opacity: 0, y: -4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} className={cn("mt-1.5 text-xs", promoMsg.ok ? "text-emerald-600 dark:text-emerald-400" : "text-destructive")}>
{promoMsg.text}
</motion.p>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{promo && (
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} className="mt-2 inline-flex items-center gap-1.5 rounded-full border border-dashed border-emerald-500/50 bg-emerald-500/10 py-1 pl-3 pr-1.5 text-xs font-semibold text-emerald-700 dark:text-emerald-300">
<Tag className="size-3" aria-hidden /> {promo.code}
<button
type="button"
onClick={() => {
setPromo(null);
setPromoMsg(null);
}}
aria-label={`Remove code ${promo.code}`}
className={cn("grid size-5 place-items-center rounded-full hover:bg-emerald-500/20", ring)}
>
<X className="size-3" />
</button>
</motion.div>
)}
</AnimatePresence>
</form>
<div className="mt-5">
<label htmlFor="cart-zone" className="text-sm font-medium">
Delivery
</label>
<div className="relative mt-1.5">
<select id="cart-zone" value={zone} onChange={(e) => setZone(e.target.value)} className={cn("h-10 w-full appearance-none rounded-lg border bg-background pl-3 pr-9 text-sm", ring)}>
{zones.map((x) => (
<option key={x.id} value={x.id}>
{x.label} — {x.cost ? fmt.format(x.cost) : "Free"}
</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
</div>
<p className="mt-1.5 text-xs text-muted-foreground">Estimated arrival: {z.eta}</p>
</div>
<label className="mt-5 flex cursor-pointer items-center gap-2.5 text-sm">
<input type="checkbox" checked={gift} onChange={(e) => setGift(e.target.checked)} className="size-4 rounded accent-[var(--primary)]" />
<Gift className="size-4 text-muted-foreground" aria-hidden /> This is a gift (add a note)
</label>
<AnimatePresence initial={false}>
{gift && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden">
<label htmlFor="cart-note" className="sr-only">
Gift note
</label>
<textarea id="cart-note" value={note} onChange={(e) => setNote(e.target.value)} rows={2} maxLength={200} placeholder="Happy birthday! Enjoy…" className={cn("mt-2 w-full rounded-lg border bg-background px-3 py-2 text-sm", ring)} />
</motion.div>
)}
</AnimatePresence>
<dl className="mt-5 space-y-2.5 border-t pt-5 text-sm">
<div className="flex justify-between">
<dt className="text-muted-foreground">Subtotal</dt>
<dd>
<AnimatedMoney value={subtotal} fmt={fmt} />
</dd>
</div>
<AnimatePresence initial={false}>
{discount > 0 && (
<motion.div key="disc" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="flex justify-between overflow-hidden text-emerald-600 dark:text-emerald-400">
<dt>Discount ({promo?.code})</dt>
<dd>
−<AnimatedMoney value={discount} fmt={fmt} />
</dd>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-between">
<dt className="text-muted-foreground">Shipping</dt>
<dd className={cn(shipping === 0 && items.length > 0 && "font-medium text-emerald-600 dark:text-emerald-400")}>{items.length === 0 ? "—" : shipping === 0 ? "Free" : <AnimatedMoney value={shipping} fmt={fmt} />}</dd>
</div>
<div className="flex justify-between text-xs">
<dt className="text-muted-foreground">Incl. VAT ({Math.round(taxRate * 100)}%)</dt>
<dd className="text-muted-foreground">
<AnimatedMoney value={tax} fmt={fmt} />
</dd>
</div>
<div className="flex items-baseline justify-between border-t pt-3">
<dt className="font-semibold">Total</dt>
<dd>
<AnimatedMoney value={total} fmt={fmt} className="text-2xl font-semibold tracking-tight" />
</dd>
</div>
{savings + discount > 0 && <p className="text-right text-xs font-medium text-emerald-600 dark:text-emerald-400">You're saving {fmt.format(savings + discount)}</p>}
</dl>
<motion.button
type="button"
onClick={checkout}
disabled={!items.length || busy}
whileTap={reduce ? undefined : { scale: 0.98 }}
className={cn("group relative mt-5 flex h-12 w-full items-center justify-center gap-2 overflow-hidden rounded-xl bg-primary text-sm font-semibold text-primary-foreground transition hover:bg-primary/90 disabled:opacity-60", ring)}
>
{!reduce && (
<motion.span
aria-hidden
className="absolute inset-y-0 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/25 to-transparent"
initial={{ left: "-40%" }}
animate={{ left: "140%" }}
transition={{ duration: 1.6, repeat: Infinity, repeatDelay: 2.5, ease: "easeInOut" }}
/>
)}
{busy ? (
<span className="size-4 animate-spin rounded-full border-2 border-primary-foreground/40 border-t-primary-foreground" aria-hidden />
) : (
<Lock className="size-4" aria-hidden />
)}
{busy ? "Redirecting to secure checkout…" : "Secure checkout"}
{!busy && <ArrowRight className="size-4 transition group-hover:translate-x-0.5" aria-hidden />}
</motion.button>
<div className="mt-4 flex items-center justify-center gap-2" aria-label="Accepted payment methods">
{["Card", "Wallet", "Bank", "Later"].map((m, i) => (
<span key={m} className="flex h-7 items-center gap-1 rounded-md border bg-background px-2 text-[10px] font-semibold text-muted-foreground">
<span className="size-2.5 rounded-sm" style={{ background: ["#6366f1", "#10b981", "#f59e0b", "#ec4899"][i] }} aria-hidden />
{m}
</span>
))}
</div>
<p className="mt-3 flex items-center justify-center gap-1.5 text-center text-[11px] text-muted-foreground">
<Lock className="size-3" aria-hidden /> 256-bit SSL encryption · 30-day returns
</p>
</div>
</aside>
</div>
</div>
</section>
);
}