"use client";
import * as React from "react";
import { AnimatePresence, animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react";
import { Check, Copy, Crown, Gift, Lock, Medal, Minus, Plus, Share2, ShoppingBag, Sparkles, Star, Ticket, Truck, Users, Wine, X, Cake, Award } from "lucide-react";
import { cn } from "@/lib/utils";
import { ProductArt, type ArtKind } from "./product-art";
export type Tier = { id: string; name: string; min: number; color: string; perks: string[] };
export type Reward = { id: string; name: string; cost: number; description: string; kind?: ArtKind; hex?: string; accent?: string; icon?: "truck" | "ticket" | "gift" | "wine"; stock?: number };
export type Activity = { id: string; label: string; detail?: string; points: number; date: string };
export interface LoyaltyRewardsProps {
memberName?: string;
programName?: string;
initialPoints?: number;
/** Lifetime points used for tier status (redeeming doesn't lower it). */
lifetimePoints?: number;
tiers?: Tier[];
rewards?: Reward[];
activity?: Activity[];
referralCode?: string;
referrals?: { joined: number; goal: number; bonus: number };
onRedeem?: (reward: Reward) => string | void;
onCopyReferral?: (code: string) => void;
className?: string;
}
const TIERS: Tier[] = [
{ id: "bronze", name: "Bronze", min: 0, color: "#b45309", perks: ["1 point per €1", "Birthday gift", "Member-only prices"] },
{ id: "silver", name: "Silver", min: 1500, color: "#94a3b8", perks: ["1.25 points per €1", "Free standard shipping", "Early access to drops"] },
{ id: "gold", name: "Gold", min: 4000, color: "#eab308", perks: ["1.5 points per €1", "Free express shipping", "Private tasting invites", "Dedicated concierge"] },
];
const REWARDS: Reward[] = [
{ id: "ship", name: "Free express delivery", cost: 300, description: "On your next order, any size.", icon: "truck" },
{ id: "v5", name: "€5 voucher", cost: 500, description: "Min. spend €30. Valid 60 days.", icon: "ticket" },
{ id: "glass", name: "Crystal nosing glass", cost: 900, description: "Hand-blown, engraved with your initials.", kind: "tumbler", hex: "#d97706", stock: 12 },
{ id: "v20", name: "€20 voucher", cost: 1800, description: "No minimum spend. Valid 90 days.", icon: "gift" },
{ id: "mini", name: "Rare cask miniature", cost: 1200, description: "5 cl sample from our private casks.", kind: "bottle", hex: "#9f1239", accent: "#fdf2f8", stock: 4 },
{ id: "class", name: "Tasting masterclass", cost: 3000, description: "Two seats, six drams, one expert host.", icon: "wine" },
];
const ACTIVITY: Activity[] = [
{ id: "a1", label: "Order LC-204981", detail: "€63.90 spent", points: 80, date: "22 Sep" },
{ id: "a2", label: "Review bonus", detail: "Highland Single Malt 12", points: 50, date: "18 Sep" },
{ id: "a3", label: "Redeemed: Free express delivery", points: -300, date: "9 Sep" },
{ id: "a4", label: "Order LC-203377", detail: "€89.59 spent", points: 112, date: "9 Sep" },
{ id: "a5", label: "Friend joined", detail: "Referral bonus — Tom W.", points: 250, date: "30 Aug" },
{ id: "a6", label: "Birthday gift", points: 200, date: "12 Aug" },
];
const ring = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
const TierIcon = ({ i, className }: { i: number; className?: string }) => {
const I = i === 0 ? Medal : i === 1 ? Award : Crown;
return <I className={className} aria-hidden />;
};
function useCount(value: number) {
const reduce = useReducedMotion();
const mv = useMotionValue(reduce ? value : 0);
const txt = useTransform(mv, (v) => Math.round(v).toLocaleString("en-US"));
React.useEffect(() => {
if (reduce) {
mv.set(value);
return;
}
const c = animate(mv, value, { duration: 1.2, ease: [0.22, 1, 0.36, 1] });
return () => c.stop();
}, [value, mv, reduce]);
return txt;
}
export function LoyaltyRewards({
memberName = "Alex",
programName = "Lumen Circle",
initialPoints = 1240,
lifetimePoints = 2860,
tiers = TIERS,
rewards = REWARDS,
activity: initialActivity = ACTIVITY,
referralCode = "ALEX-CIRCLE-25",
referrals = { joined: 2, goal: 5, bonus: 250 },
onRedeem,
onCopyReferral,
className,
}: LoyaltyRewardsProps) {
const reduce = useReducedMotion();
const [points, setPoints] = React.useState(initialPoints);
const [activity, setActivity] = React.useState(initialActivity);
const [confirm, setConfirm] = React.useState<Reward | null>(null);
const [redeemed, setRedeemed] = React.useState<{ reward: Reward; code: string } | null>(null);
const [filter, setFilter] = React.useState<"all" | "earned" | "spent">("all");
const [copied, setCopied] = React.useState(false);
const ringRef = React.useRef<HTMLDivElement>(null);
const gradId = `lr-ring-${React.useId().replace(/:/g, "")}`;
const inView = useInView(ringRef, { once: true, amount: 0.5 });
const shownPoints = useCount(inView ? points : 0);
const tierIdx = Math.max(0, tiers.map((t) => lifetimePoints >= t.min).lastIndexOf(true));
const tier = tiers[tierIdx];
const next = tiers[tierIdx + 1];
const toNext = next ? next.min - lifetimePoints : 0;
const tierPct = next ? (lifetimePoints - tier.min) / (next.min - tier.min) : 1;
const nextReward = [...rewards].sort((a, b) => a.cost - b.cost).find((r) => r.cost > points);
const ringPct = nextReward ? points / nextReward.cost : 1;
const R = 70;
const C = 2 * Math.PI * R;
const doRedeem = (r: Reward) => {
const code = onRedeem?.(r) || `${r.id.toUpperCase()}-${(points % 9000) + 1000}`;
setPoints((p) => p - r.cost);
setActivity((a) => [{ id: `n${a.length}`, label: `Redeemed: ${r.name}`, points: -r.cost, date: "Today" }, ...a]);
setConfirm(null);
setRedeemed({ reward: r, code });
};
const acts = activity.filter((a) => filter === "all" || (filter === "earned" ? a.points > 0 : a.points < 0));
return (
<section className={cn("w-full bg-background py-8 text-foreground sm:py-12", className)}>
<div className="mx-auto max-w-6xl space-y-6 px-4 sm:px-6">
{/* Header card */}
<div className="relative overflow-hidden rounded-3xl border bg-gradient-to-br from-zinc-950 via-zinc-900 to-indigo-950 p-6 text-white sm:p-8">
<div aria-hidden className="absolute -right-20 -top-24 size-72 rounded-full opacity-40 blur-3xl" style={{ background: tier.color }} />
<div aria-hidden className="absolute -bottom-24 left-1/3 size-64 rounded-full bg-indigo-500/30 blur-3xl" />
<div className="relative grid items-center gap-8 md:grid-cols-[auto_1fr]">
<div ref={ringRef} className="relative mx-auto size-44 shrink-0 sm:size-48">
<svg viewBox="0 0 160 160" className="size-full -rotate-90" aria-hidden>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="#a5b4fc" />
<stop offset="1" stopColor={tier.color} />
</linearGradient>
</defs>
<circle cx="80" cy="80" r={R} fill="none" stroke="white" strokeOpacity="0.1" strokeWidth="12" />
<motion.circle
cx="80"
cy="80"
r={R}
fill="none"
stroke={`url(#${gradId})`}
strokeWidth="12"
strokeLinecap="round"
strokeDasharray={C}
initial={{ strokeDashoffset: C }}
animate={{ strokeDashoffset: inView ? C * (1 - Math.min(1, ringPct)) : C }}
transition={{ duration: reduce ? 0 : 1.4, ease: [0.22, 1, 0.36, 1] }}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
<motion.span className="text-4xl font-semibold tabular-nums tracking-tight" aria-hidden>
{shownPoints}
</motion.span>
<span className="sr-only" aria-live="polite">
{points} points
</span>
<span className="text-xs text-white/60">points</span>
{nextReward && <span className="mt-1 max-w-[7.5rem] text-[10px] leading-tight text-white/50">{nextReward.cost - points} to {nextReward.name.toLowerCase()}</span>}
</div>
</div>
<div className="min-w-0">
<p className="text-sm text-white/60">{programName}</p>
<h2 className="mt-1 text-2xl font-semibold tracking-tight sm:text-3xl">Welcome back, {memberName}</h2>
<p className="mt-2 flex items-center gap-2 text-sm">
<span className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold text-zinc-950" style={{ background: tier.color }}>
<TierIcon i={tierIdx} className="size-3.5" /> {tier.name} member
</span>
{next && <span className="text-white/70">{toNext.toLocaleString("en-US")} pts to {next.name}</span>}
</p>
{/* Tier track */}
<div className="mt-6">
<div className="relative h-2 rounded-full bg-white/10">
<motion.div
className="absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-amber-600 via-slate-300 to-yellow-400"
initial={{ width: 0 }}
animate={{ width: inView ? `${((tierIdx + Math.min(1, tierPct)) / (tiers.length - 1)) * 100}%` : 0 }}
transition={{ duration: reduce ? 0 : 1.2, delay: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
{tiers.map((t, i) => {
const reached = i <= tierIdx;
return (
<span
key={t.id}
className={cn("absolute top-1/2 grid size-7 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border-2 transition", reached ? "border-transparent text-zinc-950" : "border-white/20 bg-zinc-900 text-white/40")}
style={{ left: `${(i / (tiers.length - 1)) * 100}%`, background: reached ? t.color : undefined }}
>
<TierIcon i={i} className="size-3.5" />
</span>
);
})}
</div>
<div className="mt-4 flex justify-between text-[11px] text-white/60">
{tiers.map((t, i) => (
<span key={t.id} className={cn(i === 0 ? "text-left" : i === tiers.length - 1 ? "text-right" : "text-center", i === tierIdx && "font-semibold text-white")}>
{t.name}
<span className="block text-white/40">{t.min.toLocaleString("en-US")}+</span>
</span>
))}
</div>
</div>
</div>
</div>
{/* Perks */}
<div className="relative mt-8 grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-white/60">Your {tier.name} perks</p>
<ul className="mt-3 space-y-2 text-sm">
{tier.perks.map((p) => (
<li key={p} className="flex items-center gap-2">
<span className="grid size-5 place-items-center rounded-full bg-emerald-400/20 text-emerald-300">
<Check className="size-3" strokeWidth={3} aria-hidden />
</span>
{p}
</li>
))}
</ul>
</div>
{next && (
<div className="rounded-2xl border border-dashed border-white/15 p-4">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-white/60">Unlock at {next.name}</p>
<ul className="mt-3 space-y-2 text-sm text-white/70">
{next.perks.map((p) => (
<li key={p} className="flex items-center gap-2">
<span className="grid size-5 place-items-center rounded-full bg-white/10">
<Lock className="size-3" aria-hidden />
</span>
{p}
</li>
))}
</ul>
</div>
)}
</div>
</div>
{/* Rewards */}
<div>
<div className="flex items-end justify-between gap-4">
<div>
<h3 className="text-xl font-semibold tracking-tight">Redeem rewards</h3>
<p className="text-sm text-muted-foreground">Spend your points on treats, vouchers and experiences.</p>
</div>
</div>
<ul className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{rewards.map((r, i) => {
const can = points >= r.cost;
const pct = Math.min(1, points / r.cost);
const Icon = r.icon === "truck" ? Truck : r.icon === "ticket" ? Ticket : r.icon === "wine" ? Wine : Gift;
return (
<motion.li
key={r.id}
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 + i * 0.05 }}
className={cn("group flex gap-4 rounded-2xl border bg-card p-4 transition", can ? "hover:-translate-y-0.5 hover:shadow-lg" : "")}
>
<span className={cn("relative grid size-16 shrink-0 place-items-center overflow-hidden rounded-xl", r.kind ? "bg-muted" : "bg-gradient-to-br from-primary/15 to-primary/5 text-primary")}>
{r.kind ? <ProductArt kind={r.kind} color={r.hex} accent={r.accent} className="p-1" /> : <Icon className="size-6" aria-hidden />}
</span>
<div className="flex min-w-0 flex-1 flex-col">
<p className="text-sm font-semibold">{r.name}</p>
<p className="text-xs text-muted-foreground">{r.description}</p>
{r.stock !== undefined && r.stock <= 5 && <p className="mt-0.5 text-[11px] font-medium text-amber-600 dark:text-amber-400">Only {r.stock} left</p>}
<div className="mt-auto flex items-center justify-between gap-2 pt-3">
<span className="flex items-center gap-1 text-sm font-semibold tabular-nums">
<Sparkles className="size-3.5 text-amber-500" aria-hidden />
{r.cost.toLocaleString("en-US")}
</span>
{can ? (
<button type="button" onClick={() => setConfirm(r)} className={cn("h-8 rounded-lg bg-foreground px-3 text-xs font-semibold text-background transition hover:bg-foreground/85", ring)}>
Redeem
</button>
) : (
<span className="flex items-center gap-2 text-[11px] text-muted-foreground">
<span className="h-1.5 w-14 overflow-hidden rounded-full bg-muted">
<span className="block h-full rounded-full bg-primary/60" style={{ width: `${pct * 100}%` }} />
</span>
{(r.cost - points).toLocaleString("en-US")} more
</span>
)}
</div>
</div>
</motion.li>
);
})}
</ul>
</div>
<div className="grid gap-6 lg:grid-cols-[1.4fr_1fr]">
{/* Activity */}
<div className="rounded-3xl border bg-card p-5 sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<h3 className="text-lg font-semibold">Points activity</h3>
<div role="tablist" aria-label="Filter activity" className="flex rounded-full bg-muted p-1 text-xs font-medium">
{(["all", "earned", "spent"] as const).map((f) => (
<button key={f} role="tab" aria-selected={filter === f} onClick={() => setFilter(f)} className={cn("relative rounded-full px-3 py-1.5 capitalize", ring)}>
{filter === f && <motion.span layoutId="lr-filter" className="absolute inset-0 rounded-full bg-background shadow-sm" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
<span className="relative">{f}</span>
</button>
))}
</div>
</div>
<ul className="mt-4 divide-y">
<AnimatePresence initial={false} mode="popLayout">
{acts.map((a) => {
const earn = a.points > 0;
const Icon = a.label.startsWith("Redeemed") ? Gift : a.label.startsWith("Review") ? Star : a.label.startsWith("Friend") ? Users : a.label.startsWith("Birthday") ? Cake : ShoppingBag;
return (
<motion.li key={a.id} layout={!reduce} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 10 }} className="flex items-center gap-3 py-3">
<span className={cn("grid size-9 shrink-0 place-items-center rounded-xl", earn ? "bg-emerald-500/12 text-emerald-600 dark:text-emerald-400" : "bg-rose-500/12 text-rose-600 dark:text-rose-400")}>
<Icon className="size-4" aria-hidden />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{a.label}</span>
<span className="block truncate text-xs text-muted-foreground">
{a.date}
{a.detail ? ` · ${a.detail}` : ""}
</span>
</span>
<span className={cn("flex items-center gap-0.5 text-sm font-semibold tabular-nums", earn ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400")}>
{earn ? <Plus className="size-3" aria-hidden /> : <Minus className="size-3" aria-hidden />}
{Math.abs(a.points).toLocaleString("en-US")}
</span>
</motion.li>
);
})}
</AnimatePresence>
</ul>
</div>
{/* Referral */}
<div className="relative overflow-hidden rounded-3xl border bg-card p-5 sm:p-6">
<div aria-hidden className="absolute -right-10 -top-10 size-40 rounded-full bg-primary/15 blur-2xl" />
<span className="relative grid size-11 place-items-center rounded-2xl bg-primary text-primary-foreground">
<Share2 className="size-5" aria-hidden />
</span>
<h3 className="relative mt-4 text-lg font-semibold">Give €10, get {referrals.bonus} points</h3>
<p className="relative mt-1 text-sm text-muted-foreground">Friends get €10 off their first order. You earn {referrals.bonus} points when they buy.</p>
<div className="relative mt-5 flex items-center gap-2 rounded-xl border-2 border-dashed bg-muted/40 p-1.5 pl-4">
<span className="min-w-0 flex-1 truncate font-mono text-sm font-semibold tracking-wider" id="lr-code">
{referralCode}
</span>
<button
type="button"
aria-describedby="lr-code"
onClick={() => {
void navigator.clipboard?.writeText(referralCode).catch(() => undefined);
onCopyReferral?.(referralCode);
setCopied(true);
window.setTimeout(() => setCopied(false), 1800);
}}
className={cn("relative flex h-9 shrink-0 items-center gap-1.5 overflow-hidden rounded-lg px-3 text-xs font-semibold transition-colors", copied ? "bg-emerald-600 text-white" : "bg-foreground text-background", ring)}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span key={String(copied)} initial={{ y: 10, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -10, opacity: 0 }} className="flex items-center gap-1.5">
{copied ? <Check className="size-3.5" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
{copied ? "Copied!" : "Copy"}
</motion.span>
</AnimatePresence>
</button>
</div>
<span className="sr-only" aria-live="polite">
{copied ? "Referral code copied" : ""}
</span>
<div className="relative mt-5">
<div className="flex items-center justify-between text-xs">
<span className="font-medium">
{referrals.joined} of {referrals.goal} friends joined
</span>
<span className="text-muted-foreground">Bonus at {referrals.goal}</span>
</div>
<div className="mt-2 flex gap-1.5">
{Array.from({ length: referrals.goal }, (_, i) => (
<motion.span
key={i}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: i * 0.08 }}
className={cn("h-2 flex-1 origin-left rounded-full", i < referrals.joined ? "bg-primary" : "bg-muted")}
/>
))}
</div>
</div>
</div>
</div>
</div>
<AnimatePresence>
{(confirm || redeemed) && (
<RedeemDialog
reward={confirm ?? redeemed!.reward}
code={redeemed?.code}
points={points}
onCancel={() => {
setConfirm(null);
setRedeemed(null);
}}
onConfirm={() => confirm && doRedeem(confirm)}
/>
)}
</AnimatePresence>
</section>
);
}
function RedeemDialog({ reward, code, points, onCancel, onConfirm }: { reward: Reward; code?: string; points: number; onCancel: () => void; onConfirm: () => void }) {
const reduce = useReducedMotion();
const ref = React.useRef<HTMLDivElement>(null);
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
const prev = document.activeElement as HTMLElement | null;
ref.current?.querySelector<HTMLElement>("[data-autofocus]")?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onCancel();
if (e.key === "Tab" && ref.current) {
const f = [...ref.current.querySelectorAll<HTMLElement>("button")];
if (e.shiftKey && document.activeElement === f[0]) {
e.preventDefault();
f[f.length - 1]?.focus();
} else if (!e.shiftKey && document.activeElement === f[f.length - 1]) {
e.preventDefault();
f[0]?.focus();
}
}
};
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
prev?.focus();
};
}, [onCancel]);
React.useEffect(() => {
ref.current?.querySelector<HTMLElement>("[data-autofocus]")?.focus();
}, [code]);
return (
<div className="fixed inset-0 z-50 grid place-items-center p-4">
<motion.div className="absolute inset-0 bg-black/55 backdrop-blur-sm" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onCancel} aria-hidden />
<motion.div
ref={ref}
role={code ? "dialog" : "alertdialog"}
aria-modal="true"
aria-labelledby="lr-dlg-title"
aria-describedby="lr-dlg-desc"
initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.92, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 380, damping: 30 }}
className="relative w-full max-w-sm rounded-3xl border bg-background p-6 text-center shadow-2xl"
>
<button type="button" onClick={onCancel} aria-label="Close" className={cn("absolute right-3 top-3 grid size-8 place-items-center rounded-full hover:bg-muted", ring)}>
<X className="size-4" />
</button>
<AnimatePresence mode="wait" initial={false}>
{!code ? (
<motion.div key="confirm" exit={{ opacity: 0, y: -8 }}>
<span className="mx-auto grid size-14 place-items-center rounded-2xl bg-primary/10 text-primary">
<Gift className="size-6" aria-hidden />
</span>
<h3 id="lr-dlg-title" className="mt-4 text-lg font-semibold">
Redeem {reward.name}?
</h3>
<p id="lr-dlg-desc" className="mt-1 text-sm text-muted-foreground">
This uses <span className="font-semibold text-foreground">{reward.cost.toLocaleString("en-US")} points</span>. You'll have {(points - reward.cost).toLocaleString("en-US")} left.
</p>
<div className="mt-6 grid grid-cols-2 gap-2">
<button type="button" onClick={onCancel} className={cn("h-11 rounded-xl border text-sm font-medium hover:bg-muted", ring)}>
Cancel
</button>
<button type="button" data-autofocus onClick={onConfirm} className={cn("h-11 rounded-xl bg-primary text-sm font-semibold text-primary-foreground hover:bg-primary/90", ring)}>
Confirm
</button>
</div>
</motion.div>
) : (
<motion.div key="done" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }}>
<motion.span initial={reduce ? false : { scale: 0, rotate: -30 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: "spring", stiffness: 400, damping: 14 }} className="mx-auto grid size-14 place-items-center rounded-full bg-emerald-500 text-white">
<Check className="size-7" strokeWidth={3} aria-hidden />
</motion.span>
<h3 id="lr-dlg-title" className="mt-4 text-lg font-semibold">
Reward unlocked!
</h3>
<p id="lr-dlg-desc" className="mt-1 text-sm text-muted-foreground">
Use this code at checkout for your {reward.name.toLowerCase()}.
</p>
<div className="mt-5 flex items-center gap-2 rounded-xl border-2 border-dashed border-emerald-500/40 bg-emerald-500/5 p-1.5 pl-4">
<span className="flex-1 text-left font-mono font-semibold tracking-wider">{code}</span>
<button
type="button"
data-autofocus
onClick={() => {
void navigator.clipboard?.writeText(code).catch(() => undefined);
setCopied(true);
}}
className={cn("flex h-9 items-center gap-1.5 rounded-lg bg-foreground px-3 text-xs font-semibold text-background", ring)}
>
{copied ? <Check className="size-3.5" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
{copied ? "Copied" : "Copy"}
</button>
</div>
<button type="button" onClick={onCancel} className={cn("mt-4 h-11 w-full rounded-xl border text-sm font-medium hover:bg-muted", ring)}>
Done
</button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
);
}