# Zeno Progress: Image Generation

> A prompt card. Generate runs Composing, Denoising and Upscaling while the model takes a random 4 to 9 seconds, so the bar is often deep in the ruler when the image arrives; the gap snaps shut and the image resolves in. Generate again for another take.

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

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/zeno-progress-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/zeno-progress-demo-light-8efa59d996e9f52a.png
- Registry item: https://keyframeui.com/r/zeno-progress-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-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-demo

## Files

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

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";

import { ZenoProgress } from "@/components/ui/zeno-progress";

const prompts = [
  {
    text: "Alpine ridge at first light, low cloud pooled in the valley, 35mm film",
    image: "1464822759023-fed622ff2c3b",
  },
  {
    text: "Still lake under a pine slope, late summer, soft overcast light",
    image: "1501785888041-af3ef285b470",
  },
  {
    text: "Snowy peaks under a clear night sky, long exposure, cold blue tones",
    image: "1519681393784-d120267933ba",
  },
];

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=960&h=600&q=80`;

type Phase = "idle" | "running" | "finishing" | "shown";

export function ZenoProgressDemo() {
  const [phase, setPhase] = React.useState<Phase>("idle");
  const [take, setTake] = React.useState(0);
  const prompt = prompts[take % prompts.length];

  // The "model" answers after 4–9 s; the bar only finishes when it does.
  React.useEffect(() => {
    if (phase !== "running") return;
    const timer = window.setTimeout(() => setPhase("finishing"), 4000 + Math.random() * 5000);
    return () => window.clearTimeout(timer);
  }, [phase, take]);

  // Load the next image while the bar runs, so it's ready for the reveal.
  React.useEffect(() => {
    const next = new Image();
    next.src = photo(prompt.image);
  }, [prompt.image]);

  const generate = () => {
    if (phase === "shown") setTake((t) => t + 1);
    setPhase("running");
  };

  const busy = phase === "running" || phase === "finishing";

  return (
    <div className="w-full max-w-md rounded-2xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
      <div className="relative aspect-[8/5] overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-900">
        <AnimatePresence initial={false}>
          {phase === "shown" ? (
            <motion.img
              key={prompt.image}
              src={photo(prompt.image)}
              alt={prompt.text}
              className="absolute inset-0 size-full object-cover"
              initial={{ opacity: 0, scale: 1.06, filter: "blur(18px)" }}
              animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
              exit={{ opacity: 0, transition: { duration: 0.25 } }}
              transition={{ type: "spring", visualDuration: 0.7, bounce: 0 }}
            />
          ) : null}
        </AnimatePresence>
        <AnimatePresence initial={false}>
          {phase !== "shown" ? (
            <motion.div
              key="empty"
              className="absolute inset-0 flex items-end p-4"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0, transition: { duration: 0.3 } }}
            >
              <div className="w-full">
                {busy ? (
                  <ZenoProgress
                    key={take}
                    done={phase === "finishing"}
                    steps={["Composing", "Denoising", "Upscaling"]}
                    doneLabel="Ready"
                    interval={900}
                    onComplete={() => window.setTimeout(() => setPhase("shown"), 220)}
                  />
                ) : (
                  <p className="text-sm text-zinc-500 dark:text-zinc-400">Your image appears here.</p>
                )}
              </div>
            </motion.div>
          ) : null}
        </AnimatePresence>
      </div>

      <div className="mt-3 flex items-end gap-3 px-1 pb-1">
        <div className="min-w-0 flex-1">
          <p className="text-xs font-medium text-zinc-400 dark:text-zinc-500">Prompt</p>
          <p className="mt-1 text-sm leading-snug text-zinc-800 dark:text-zinc-200">{prompt.text}</p>
        </div>
        <button
          type="button"
          onClick={generate}
          disabled={busy}
          className="shrink-0 rounded-full bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition-[opacity,transform] active:scale-[0.97] disabled:opacity-40 dark:bg-zinc-100 dark:text-zinc-900"
        >
          {phase === "shown" ? "Generate again" : "Generate"}
        </button>
      </div>
    </div>
  );
}
```
