"use client";
import * as React from "react";
import { RotateCcw } from "lucide-react";
import { OutcomeSpinnerLoader } from "@/components/ui/outcome-spinner-loader";
import type { SpinnerStatus } from "@/components/ui/outcome-spinner-loader";
type DemoProps = { autoplay?: boolean; bare?: boolean; status?: SpinnerStatus; progress?: number; speed?: number };
const CARD = "flex items-center gap-3 rounded-2xl border border-border bg-card px-4 py-3";
export default function OutcomeSpinnerLoaderDemo({ autoplay = true, bare = false, status: preset, progress: presetProgress, speed = 1 }: DemoProps) {
const [upload, setUpload] = React.useState(preset ?? "idle");
const [uploadProgress, setUploadProgress] = React.useState(presetProgress ?? 0);
const [save, setSave] = React.useState(preset ?? "idle");
const [sync, setSync] = React.useState(preset ?? "idle");
const [button, setButton] = React.useState(preset ?? "idle");
const [settled, setSettled] = React.useState([] as string[]);
const [motion, setMotion] = React.useState({ ready: false, reduced: true });
const [run, setRun] = React.useState(0);
const [paused, setPaused] = React.useState(false);
const ms = (at: number) => Math.round(at * speed);
React.useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setMotion({ ready: true, reduced: media.matches });
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
React.useEffect(() => {
if (!motion.ready || !autoplay || motion.reduced || paused || preset !== undefined) return;
const timers: number[] = [];
const later = (at: number, callback: () => void) => timers.push(window.setTimeout(callback, ms(at)));
later(300, () => { setUpload("pending"); setSave("pending"); setSync("pending"); });
later(1000, () => setButton("pending"));
later(1600, () => { setUpload("progress"); setUploadProgress(0); });
for (let step = 1; step <= 12; step += 1) later(1600 + step * 200, () => setUploadProgress(Math.min(100, Math.round(step * 8.4))));
later(4150, () => setUploadProgress(100));
later(4300, () => setUpload("success"));
later(2600, () => setSave("success"));
later(3400, () => setSync("error"));
later(3000, () => setButton("success"));
later(8600, () => { setUpload("idle"); setUploadProgress(0); setSave("idle"); setSync("idle"); setButton("idle"); setSettled([]); setRun((previous) => previous + 1); });
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [motion.ready, motion.reduced, autoplay, paused, run, preset, speed]);
const takeOver = () => autoplay && setPaused(true);
return (
<main className="flex min-h-svh w-full flex-col items-center justify-center gap-5 bg-background px-5 py-6 text-foreground" onPointerDownCapture={takeOver} onKeyDownCapture={takeOver}>
{!bare && (
<header className="w-full max-w-[440px]">
<p className="mb-1 text-[10px] font-medium uppercase tracking-[0.2em] text-muted-foreground">From waiting to the result</p>
<h1 className="text-[24px] font-semibold leading-tight tracking-tight">One spinner, all the way to done.</h1>
</header>
)}
<section className="w-full max-w-[440px] space-y-3">
<div data-card="upload" className={CARD}>
<OutcomeSpinnerLoader status={upload} progress={uploadProgress} size="lg" showPercent label="Uploading" labels={{ idle: "Ready to upload", pending: "Preparing upload…", progress: `Uploading · ${uploadProgress}%`, success: "Upload complete", error: "Upload failed" }} onSettled={(outcome) => setSettled((previous) => [...previous, `upload:${outcome}`])} />
<span className="ml-auto font-mono text-[11px] text-muted-foreground">report.pdf</span>
</div>
<div data-card="save" className={CARD}>
<OutcomeSpinnerLoader status={save} size="md" label="Saving" labels={{ idle: "Unsaved changes", pending: "Saving…", success: "Saved", error: "Not saved" }} onSettled={(outcome) => setSettled((previous) => [...previous, `save:${outcome}`])} />
<span className="ml-auto font-mono text-[11px] text-muted-foreground">draft</span>
</div>
<div data-card="sync" className={CARD}>
<OutcomeSpinnerLoader status={sync} size="md" label="Syncing" labels={{ idle: "Sync paused", pending: "Syncing…", success: "In sync", error: "Sync failed · offline" }} onSettled={(outcome) => setSettled((previous) => [...previous, `sync:${outcome}`])} />
<span className="ml-auto font-mono text-[11px] text-muted-foreground">3 changes</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-border bg-card px-4 py-3">
<span className="text-xs text-muted-foreground">Inline in a button</span>
<button type="button" data-button onClick={() => { setButton("pending"); window.setTimeout(() => setButton("success"), ms(1600)); }} disabled={button === "pending"} className="inline-flex h-9 items-center gap-2 rounded-xl bg-primary px-3.5 text-sm font-medium text-primary-foreground outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card disabled:opacity-80">
{button !== "idle" && <OutcomeSpinnerLoader status={button} size="sm" tone="inherit" label="Saving" />}
{button === "success" ? "Saved" : button === "pending" ? "Saving…" : "Save changes"}
</button>
</div>
<p data-settled className="sr-only">{settled.join(",")}</p>
</section>
{!bare && (
<footer className="flex w-full max-w-[440px] items-center justify-between text-[10px] text-muted-foreground">
<span>Sample requests · nothing is uploaded</span>
<button type="button" data-replay onClick={() => { setUpload("idle"); setUploadProgress(0); setSave("idle"); setSync("idle"); setButton("idle"); setSettled([]); setPaused(false); setRun((previous) => previous + 1); }} className="inline-flex items-center gap-1 rounded px-1 underline decoration-border underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><RotateCcw className="size-3" aria-hidden="true" />Replay</button>
</footer>
)}
</main>
);
}