# Counter Curtain: Launch Page

> A custom counter in the drop's colors, with a padded number and a progress bar, on a curtain that falls away. `onComplete` starts the countdown as it goes, so the clock is already ticking when it's uncovered.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/counter-curtain-launch-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/counter-curtain-launch-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/counter-curtain-launch-demo-light-495e4d47ca44fc7b.png
- Registry item: https://keyframeui.com/r/counter-curtain-launch-demo.json
- Categories: preloader, hero, text
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/counter-curtain-launch-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/counter-curtain.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/counter-curtain#counter-curtain-launch-demo

## Files

### `registry/demos/counter-curtain-launch-demo.tsx`

```tsx
"use client";

import * as React from "react";

import {
  CounterCurtain,
  CounterCurtainLine,
  CounterCurtainValue,
} from "@/components/ui/counter-curtain";

const photo =
  "https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?auto=format&fit=crop&w=1000&h=1000&q=80";

const DROP = 2 * 86400 + 14 * 3600 + 9 * 60 + 31; // seconds left when the page opens

// A custom counter: the drop's own type, a padded number and a progress bar
// driven by --counter-progress. onComplete starts the clock as the curtain
// lifts, so it's already ticking when it's uncovered.
export function CounterCurtainLaunchDemo() {
  const [run, setRun] = React.useState(0);
  const [live, setLive] = React.useState(false);

  const replay = () => {
    setLive(false);
    setRun((r) => r + 1);
  };

  return (
    <div className="flex h-[var(--demo-height,36rem)] w-full flex-col overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-950 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50">
      <BrowserBar url="volt.run/drop-04" run={run} onReload={replay} />
      <CounterCurtain
        key={run}
        contained
        once={false}
        direction="down"
        onComplete={() => setLive(true)}
        curtainClassName="bg-lime-300 text-lime-950 dark:bg-lime-300"
        counter={
          <div className="flex h-full flex-col justify-between p-5 sm:p-8">
            <div className="flex justify-between text-[11px] font-semibold tracking-[0.2em] uppercase">
              <span>Volt Trainer</span>
              <span>Drop 04</span>
            </div>
            <CounterCurtainValue
              pad={3}
              className="text-center text-[clamp(5rem,30cqw,17rem)] leading-none font-black tracking-[-0.07em]"
            />
            <div className="h-1.5 overflow-hidden rounded-full bg-lime-950/10">
              <div className="h-full origin-left scale-x-[var(--counter-progress,0)] rounded-full bg-lime-950" />
            </div>
          </div>
        }
        className="@container min-h-0 flex-1"
      >
        <div className="flex h-full flex-col px-5 pt-4 pb-5 sm:px-8 sm:pt-5 sm:pb-7">
          <header className="flex items-center justify-between text-sm">
            <CounterCurtainLine className="font-black tracking-tight">VOLT</CounterCurtainLine>
            <CounterCurtainLine className="text-zinc-500">Drop 04 · 500 pairs</CounterCurtainLine>
          </header>

          <div className="flex min-h-0 flex-1 items-center gap-6 @2xl:gap-10">
            <div className="min-w-0 flex-1">
              <h1 className="text-[clamp(3.25rem,13cqw,9rem)] leading-[0.86] font-black tracking-[-0.06em] uppercase">
                <CounterCurtainLine>Volt</CounterCurtainLine>
                <CounterCurtainLine>Trainer</CounterCurtainLine>
              </h1>
              <CounterCurtainLine className="mt-5 max-w-sm text-sm leading-relaxed text-pretty text-zinc-600 sm:mt-6 dark:text-zinc-400">
                A flat, grippy sole and a heel that locks in for lifts. In lime, for one
                weekend only.
              </CounterCurtainLine>
              <div className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-4 sm:mt-8">
                <CounterCurtainLine>
                  <Countdown live={live} />
                </CounterCurtainLine>
                <CounterCurtainLine>
                  <button
                    type="button"
                    className="h-10 rounded-full bg-lime-300 px-5 text-sm font-semibold text-lime-950 transition-[background-color,scale] duration-150 hover:bg-lime-200 active:scale-[0.97]"
                  >
                    Notify me
                  </button>
                </CounterCurtainLine>
              </div>
            </div>
            <CounterCurtainLine className="hidden w-[38%] shrink-0 rounded-2xl @xl:block">
              <img
                src={photo}
                alt="A lime and black trainer"
                className="aspect-square w-full rounded-2xl object-cover"
              />
            </CounterCurtainLine>
          </div>
        </div>
      </CounterCurtain>
    </div>
  );
}

function Countdown({ live }: { live: boolean }) {
  const [left, setLeft] = React.useState(DROP);

  React.useEffect(() => {
    if (!live) return;
    const timer = window.setInterval(() => setLeft((s) => Math.max(0, s - 1)), 1000);
    return () => window.clearInterval(timer);
  }, [live]);

  const parts = [
    [Math.floor(left / 86400), "d"],
    [Math.floor((left % 86400) / 3600), "h"],
    [Math.floor((left % 3600) / 60), "m"],
    [left % 60, "s"],
  ] as const;

  return (
    <p className="flex items-center gap-3 text-sm">
      <span className="relative flex size-2">
        {live ? <span className="absolute inset-0 animate-ping rounded-full bg-lime-400" /> : null}
        <span className="relative size-2 rounded-full bg-lime-500" />
      </span>
      <span className="text-zinc-500">Drops in</span>
      <span className="font-semibold tabular-nums">
        {parts.map(([value, unit]) => (
          <span key={unit} className="mr-1.5 last:mr-0">
            {String(value).padStart(2, "0")}
            <span className="text-zinc-400">{unit}</span>
          </span>
        ))}
      </span>
    </p>
  );
}

function BrowserBar({ url, run, onReload }: { url: string; run: number; onReload: () => void }) {
  return (
    <div className="flex h-10 shrink-0 items-center gap-3 border-b border-zinc-200 bg-zinc-50 px-3 dark:border-zinc-800 dark:bg-zinc-900/60">
      <div aria-hidden className="flex gap-1.5">
        {[0, 1, 2].map((dot) => (
          <span key={dot} className="size-2.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
        ))}
      </div>
      <button
        type="button"
        onClick={onReload}
        aria-label="Reload to play it again"
        title="Play again"
        className="grid size-7 place-items-center rounded-md text-zinc-500 transition-[background-color,color,scale] duration-150 hover:bg-zinc-200/70 hover:text-zinc-900 active:scale-[0.94] dark:hover:bg-zinc-800 dark:hover:text-zinc-100"
      >
        <svg
          viewBox="0 0 16 16"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          className="size-3.5 transition-[rotate] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
          style={{ rotate: `${run * 360}deg` }}
        >
          <path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9" />
          <path d="M13.5 2.5v3h-3" />
        </svg>
      </button>
      <span className="mx-auto truncate rounded-md bg-zinc-100 px-3 py-1 font-mono text-[11px] text-zinc-500 dark:bg-zinc-800/80 dark:text-zinc-400">
        {url}
      </span>
      <span aria-hidden className="w-[4.75rem]" />
    </div>
  );
}
```
