# Zeno Progress: Upload List

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/zeno-progress-upload-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/zeno-progress-upload-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/zeno-progress-upload-demo-light-c6bcb0759efb2548.png
- Registry item: https://keyframeui.com/r/zeno-progress-upload-demo.json
- Categories: progress, loading, ai, canvas
- Listed: 2026-10-23, source last changed 2026-10-23

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/zeno-progress-upload-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/zeno-progress.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/zeno-progress#zeno-progress-upload-demo

## Files

### `registry/demos/zeno-progress-upload-demo.tsx`

```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>
  );
}
```
