Three files, each with its own bar, finishing at different times. The video's virus scan takes much longer, so you can watch its ruler go many decimals deep while the others are done.
registry/demos/zeno-progress-upload-demo.tsx
"use client";
import * as React from "react";
import { useInView } from "motion/react";
import { cn } from "@/lib/utils";
import { ZenoProgress } from "@/components/ui/zeno-progress";
const files = [
{ name: "brand-guidelines.pdf", size: "4.2 MB", kind: "PDF", ms: 2600, interval: 600 },
{ name: "hero-loop.mp4", size: "86.1 MB", kind: "MP4", ms: 21000, interval: 900 },
{ name: "Q3-review.key", size: "18.9 MB", kind: "KEY", ms: 6400, interval: 750 },
];
export function ZenoProgressUploadDemo() {
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.4 });
const [batch, setBatch] = React.useState(0);
const [finished, setFinished] = React.useState<string[]>([]);
// Each file's server answers on its own schedule; the video takes far longer.
React.useEffect(() => {
if (!inView) return;
const timers = files.map((file) =>
window.setTimeout(() => setFinished((names) => [...names, file.name]), file.ms),
);
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [inView, batch]);
const allDone = finished.length === files.length;
const again = () => {
setFinished([]);
setBatch((b) => b + 1);
};
return (
<div
ref={ref}
className="w-full max-w-md rounded-2xl border border-zinc-200 bg-white p-2 shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
>
<div className="flex items-center justify-between px-3 pt-2 pb-3">
<div>
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">Uploads</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
{finished.length} of {files.length} ready
</p>
</div>
<button
type="button"
onClick={again}
disabled={!allDone}
className="rounded-full border border-zinc-200 px-3 py-1 text-xs font-medium text-zinc-700 transition-[opacity,transform] hover:bg-zinc-50 active:scale-[0.97] disabled:opacity-40 dark:border-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-900"
>
Upload again
</button>
</div>
<ul className="space-y-1">
{files.map((file) => {
const done = finished.includes(file.name);
return (
<li
key={file.name}
className="flex items-center gap-3 rounded-xl bg-zinc-50 px-3 py-3 dark:bg-zinc-900/60"
>
<span
aria-hidden
className={cn(
"grid size-9 shrink-0 place-items-center rounded-lg border text-[10px] font-semibold tracking-wide transition-colors duration-300",
done
? "border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900"
: "border-zinc-200 bg-white text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-400",
)}
>
{file.kind}
</span>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-baseline justify-between gap-3">
<p className="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{file.name}</p>
<p className="shrink-0 text-xs text-zinc-400 dark:text-zinc-500">{file.size}</p>
</div>
<ZenoProgress
key={`${batch}-${inView}`}
done={done}
steps={["Uploading", "Scanning for viruses"]}
doneLabel="Uploaded"
interval={file.interval}
className="text-xs text-zinc-500 dark:text-zinc-400"
/>
</div>
</li>
);
})}
</ul>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/zeno-progress#zeno-progress-upload-demo