"use client";
import * as React from "react";
import { ConfirmPanel, UsdcIcon, type ConfirmPanelProps } from "@/components/ui/swipe-to-confirm";
/* Live controls (21st Demo Controls). */
const settings = {
darkMode: false,
mobileView: false,
};
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
type Scenario = { id: string; label: string; panel: (dark: boolean, attempt: { n: number }) => Omit<ConfirmPanelProps, "theme"> };
const SCENARIOS: Scenario[] = [
{
id: "send",
label: "Send",
panel: () => ({
title: "Send",
caption: "Balance $2,480.00",
party: { name: "Alex Morgan", detail: "0x7a3fโฆc91e" },
amount: { value: "250", token: { symbol: "USDC", icon: <UsdcIcon /> }, fiat: "โ $250.00" },
rows: [
{ label: "Network", value: "Base" },
{ label: "Fee", value: "$0.02" },
{ label: "Arrives", value: "~ 5 sec" },
],
swipe: { label: "Slide to send", doneLabel: "Sent", onConfirm: () => wait(1400) },
}),
},
{
id: "pay",
label: "Pay",
panel: (dark, attempt) => ({
title: "Checkout",
party: { name: "Pro plan", detail: "Billed yearly" },
amount: { value: "$144" },
rows: [
{ label: "Card", value: "Visa โขโขโขโข 4242" },
{ label: "Renews", value: "Oct 20, 2027" },
],
swipe: {
label: "Slide to pay",
doneLabel: "Paid",
color: dark ? "#5E5CE6" : "#5856D6",
onConfirm: async () => {
await wait(1100);
if (attempt.n++ === 0) throw new Error("Card declined. Try again.");
},
},
}),
},
{
id: "delete",
label: "Delete",
panel: (dark) => ({
title: "Delete workspace",
party: { name: "Acme Studio", detail: "12 members ยท 3 projects" },
hero: (
<div className="rounded-[14px] px-3.5 py-3 text-[15px] leading-[21px]" style={{ background: dark ? "rgba(255,69,58,.16)" : "rgba(255,59,48,.1)", color: dark ? "#FF6961" : "#D70015" }}>
This deletes every project and file for all members. You can't undo it.
</div>
),
swipe: {
label: "Slide to delete",
doneLabel: "Deleted",
color: dark ? "#FF453A" : "#FF3B30",
onConfirm: () => wait(1200),
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3" />
</svg>
),
},
}),
},
];
/* HIG segmented control */
function Segmented({ items, value, onChange, dark }: { items: { id: string; label: string }[]; value: string; onChange: (id: string) => void; dark: boolean }) {
const i = Math.max(0, items.findIndex((x) => x.id === value));
return (
<div role="tablist" className="relative grid h-8 rounded-[9px] p-[2px]" style={{ gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))`, width: items.length * 84, background: dark ? "rgba(118,118,128,.24)" : "rgba(118,118,128,.12)" }}>
<div
className="absolute bottom-[2px] left-[2px] top-[2px] rounded-[7px] transition-transform duration-300 [transition-timing-function:cubic-bezier(.2,.9,.25,1)]"
style={{ width: `calc((100% - 4px) / ${items.length})`, transform: `translateX(${i * 100}%)`, background: dark ? "#636366" : "#fff", boxShadow: "0 3px 8px rgba(0,0,0,.12), 0 3px 1px rgba(0,0,0,.04)" }}
/>
{items.map((it) => (
<button key={it.id} role="tab" type="button" aria-selected={it.id === value} onClick={() => onChange(it.id)} className="relative z-10 text-[13px] outline-none" style={{ color: dark ? "#fff" : "#000", fontWeight: it.id === value ? 600 : 500 }}>
{it.label}
</button>
))}
</div>
);
}
export default function SwipeToConfirmDemo(props: Partial<typeof settings>) {
const s = { ...settings, ...props };
const dark = s.darkMode;
const [id, setId] = React.useState("send");
const [run, setRun] = React.useState(0); // remounts the panel = fresh state
const [open, setOpen] = React.useState(true);
const attempt = React.useRef({ n: 0 });
const sc = SCENARIOS.find((x) => x.id === id) ?? SCENARIOS[0];
React.useEffect(() => {
attempt.current.n = 0;
setOpen(true);
setRun((r) => r + 1);
}, [id, s.mobileView]);
const panel = sc.panel(dark, attempt.current);
const theme = dark ? "dark" : "light";
return (
<div
className="flex min-h-screen w-full flex-col items-center justify-center gap-6 px-4 py-10"
style={{
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', Inter, 'Segoe UI', Roboto, sans-serif",
/* dot grid from the "work with AI" block of vasnev.art */
backgroundColor: dark ? "#000" : "#F2F2F7",
backgroundImage: `radial-gradient(circle, ${dark ? "rgba(255,255,255,.07)" : "rgba(0,0,0,.07)"} 0 1px, transparent 1.4px)`,
backgroundSize: "20px 13px",
backgroundPosition: "-6px -1px",
}}
>
<Segmented items={SCENARIOS} value={id} onChange={setId} dark={dark} />
{s.mobileView ? (
/* phone frame: the same panel as a bottom sheet */
<div
className="relative h-[720px] w-[360px] max-w-full overflow-hidden rounded-[46px]"
style={{ background: dark ? "#000" : "#F2F2F7", boxShadow: `0 0 0 6px ${dark ? "#2c2c2e" : "#1c1c1e"}, 0 30px 80px rgba(0,0,0,.25)` }}
>
<div className="absolute inset-0 grid place-items-center">
<button
type="button"
onClick={() => {
setRun((r) => r + 1);
setOpen(true);
}}
className="h-[50px] rounded-[12px] px-6 text-[17px] font-semibold text-white transition-transform active:scale-[.97]"
style={{ background: panel.swipe?.color ?? (dark ? "#0A84FF" : "#007AFF") }}
>
{panel.swipe?.label?.replace("Slide to ", "").replace(/^./, (c) => c.toUpperCase())}
</button>
</div>
<ConfirmPanel key={`${id}-${run}`} {...panel} theme={theme} presentation="sheet" sheetContainer="parent" open={open} onOpenChange={setOpen} />
</div>
) : (
<ConfirmPanel key={`${id}-${run}`} {...panel} theme={theme} presentation="card" swipe={{ ...panel.swipe, resetAfter: 2600 }} />
)}
<p className="m-0 max-w-[360px] text-center text-[13px] leading-[18px]" style={{ color: dark ? "rgba(235,235,245,.6)" : "rgba(60,60,67,.6)" }}>
{id === "pay" ? "The first try is declined on purpose, to show the error state." : "Drag the circle to the end, or focus it and press Enter. On phones the same panel opens as a bottom sheet."}
</p>
</div>
);
}