"use client"; import { MinusIcon, PlusIcon, ShoppingBagIcon, Trash2Icon, TruckIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, type ReactNode, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface CartLine { id: string; name: string; variant?: string; price: number; qty: number; art?: ReactNode; max?: number; } export interface CartDrawerProps { items?: CartLine[]; defaultItems?: CartLine[]; onItemsChange?: (items: CartLine[]) => void; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; freeShippingAt?: number; currency?: string; locale?: string; onCheckout?: (items: CartLine[], subtotal: number) => void; children?: (cartButton: ReactNode) => ReactNode; className?: string; } export function CartDrawer({ items: itemsProp, defaultItems = [], onItemsChange, open: openProp, defaultOpen = false, onOpenChange, freeShippingAt, currency = "USD", locale = "en-US", onCheckout, children, className, }: CartDrawerProps) { const reduce = useReducedMotion(); const [innerItems, setInnerItems] = useState(defaultItems); const [innerOpen, setInnerOpen] = useState(defaultOpen); const items = itemsProp ?? innerItems; const open = openProp ?? innerOpen; const triggerRef = useRef(null); const panelRef = useRef(null); const opened = useRef(false); const setItems = (next: CartLine[]) => { if (itemsProp === undefined) setInnerItems(next); onItemsChange?.(next); }; const setOpen = (next: boolean) => { if (openProp === undefined) setInnerOpen(next); onOpenChange?.(next); opened.current = next; if (!next) requestAnimationFrame(() => triggerRef.current?.focus()); }; useEffect(() => { if (open && opened.current) panelRef.current?.focus(); }, [open]); const fmt = (n: number) => new Intl.NumberFormat(locale, { style: "currency", currency }).format(n); const count = items.reduce((n, l) => n + l.qty, 0); const subtotal = items.reduce((n, l) => n + l.qty * l.price, 0); const remaining = freeShippingAt ? Math.max(0, freeShippingAt - subtotal) : 0; const unlocked = !!freeShippingAt && remaining === 0; const setQty = (id: string, qty: number) => setItems(qty <= 0 ? items.filter((l) => l.id !== id) : items.map((l) => (l.id === id ? { ...l, qty } : l))); const onKey = (event: KeyboardEvent) => { if (event.key === "Escape" && open) { event.stopPropagation(); setOpen(false); return; } const panel = panelRef.current; if (event.key !== "Tab" || !open || !panel) return; const items = [...panel.querySelectorAll("button:not(:disabled)")]; const first = items[0]; const last = items[items.length - 1]; const at = document.activeElement; if (event.shiftKey && (at === first || at === panel)) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && at === last) { event.preventDefault(); first?.focus(); } }; const ease = [0.32, 0.72, 0, 1] as const; const cartButton = ( ); return (
{children?.(cartButton) ??
{cartButton}
}
{open && ( <> setOpen(false)} transition={{ duration: 0.2 }} />

Your bag ({count})

{freeShippingAt && items.length > 0 && (
{unlocked ? ( "Free shipping unlocked" ) : ( {fmt(remaining)} away from free shipping )}
)}
{items.length === 0 ? (

Your bag is empty

Things you add show up here, ready when you are.

) : (
    {items.map((line) => (
    {line.art}

    {line.name}

    {line.variant &&

    {line.variant}

    }
    setQty(line.id, q)} value={line.qty} />
    ))}
)}
{items.length > 0 && (
Subtotal
Shipping {unlocked ? "Free" : "Calculated at checkout"}
)}
)}
); } function Stepper({ value, onChange, max, label }: { value: number; onChange: (v: number) => void; max: number; label: string }) { return (
); } function RollingNumber({ text, className }: { text: string; className?: string }) { const reduce = useReducedMotion(); const chars = text.split(""); return ( {chars.map((c, i) => { const key = chars.length - i; if (!/\d/.test(c)) { return ( {c} ); } return ( {Array.from({ length: 10 }, (_, d) => ( {d} ))} ); })} ); }