{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "countdown",
  "title": "Countdown",
  "description": "A live countdown timer to a future moment — it re-renders every second, ticks down, never goes negative, and fires an onComplete callback once when it reaches zero. Use it wherever you're waiting on a deadline: a product/waitlist launch or \"coming soon\" page, a sale/offer/flash-deal or cart-reservation expiry, an OTP/verification resend or rate-limit cooldown, an auction or bid close, a webinar/event/stream start time, a maintenance window, a booking or checkout hold, or a quiz/game round timer. Pass `to` as a Date, an ISO string, or epoch milliseconds. By default it renders labeled days/hours/minutes/seconds segments (the days block appears only once at least a day remains, or force it with showDays) using shadcn card/border/foreground tokens with tabular-nums so digits don't jitter. For a fully custom face — a compact \"02:14:33\", a circular ring, marketing hero digits — pass a render-prop child that receives { days, hours, minutes, seconds, total, isComplete } and return your own markup. shadcn/ui ships no countdown or timer component; this is the future-facing counterpart to a relative \"time ago\" label. It's SSR/hydration-safe (server and first client render agree, then a real clock takes over) and accessible: role=timer with an aria-atomic sr-only sentence (\"2 days, 14 hours, 33 minutes remaining\") while the visual segments are aria-hidden, so screen readers can read the state without being spammed each second. Tune the tick with interval (100 for smooth, 60000 for minute-only) and swap the finished view with completedLabel. Depends only on your cn util — no date library, no extra packages.",
  "files": [
    {
      "path": "registry/ui/countdown.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** The remaining time, already broken into whole units. `total` is milliseconds left (clamped at 0). */\nexport interface CountdownParts {\n  days: number\n  hours: number\n  minutes: number\n  seconds: number\n  /** Milliseconds remaining, never negative. */\n  total: number\n  /** True once the target time has been reached. */\n  isComplete: boolean\n}\n\ninterface CountdownProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** The moment to count down to. Accepts a Date, an ISO string, or epoch milliseconds. */\n  to: Date | string | number\n  /** Fired once, when the timer reaches zero. */\n  onComplete?: () => void\n  /** Tick cadence in ms (default 1000). Use 100 for a smoother seconds display, 60000 to only track minutes. */\n  interval?: number\n  /** Always render the days segment even when zero (default false — days appear only once there is at least a day left). */\n  showDays?: boolean\n  /** Render prop for full control: receives the live CountdownParts and returns your own markup. */\n  children?: (parts: CountdownParts) => React.ReactNode\n  /** What to show once complete when using the default rendering (default \"00:00:00\" style zeros). */\n  completedLabel?: React.ReactNode\n}\n\nconst MS = { day: 86_400_000, hour: 3_600_000, minute: 60_000, second: 1_000 }\n\nfunction partsFrom(targetMs: number, nowMs: number): CountdownParts {\n  const total = Math.max(0, targetMs - nowMs)\n  return {\n    days: Math.floor(total / MS.day),\n    hours: Math.floor((total % MS.day) / MS.hour),\n    minutes: Math.floor((total % MS.hour) / MS.minute),\n    seconds: Math.floor((total % MS.minute) / MS.second),\n    total,\n    isComplete: total <= 0,\n  }\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\")\n\n/** Build a spoken sentence for screen readers — \"2 days, 14 hours, 33 minutes, 7 seconds remaining\". */\nfunction spoken(p: CountdownParts): string {\n  if (p.isComplete) return \"Time's up\"\n  const unit = (n: number, label: string) =>\n    `${n} ${label}${n === 1 ? \"\" : \"s\"}`\n  const bits: string[] = []\n  if (p.days) bits.push(unit(p.days, \"day\"))\n  if (p.days || p.hours) bits.push(unit(p.hours, \"hour\"))\n  bits.push(unit(p.minutes, \"minute\"), unit(p.seconds, \"second\"))\n  return `${bits.join(\", \")} remaining`\n}\n\n/**\n * A live countdown to a future moment — launches, sale/offer deadlines, OTP\n * resend cooldowns, auction or event start/end. It re-renders on a timer,\n * fires onComplete once at zero, and never goes negative. shadcn/ui ships no\n * countdown/timer component; this is the future-facing counterpart to a\n * relative \"time ago\" label.\n */\nexport const Countdown = React.forwardRef<HTMLDivElement, CountdownProps>(\n  function Countdown(\n    {\n      className,\n      to,\n      onComplete,\n      interval = 1000,\n      showDays = false,\n      children,\n      completedLabel,\n      ...props\n    },\n    ref\n  ) {\n    const target = React.useMemo(() => new Date(to), [to])\n    const targetMs = target.getTime()\n    const valid = !Number.isNaN(targetMs)\n\n    // Starts null so server and first client render agree; the effect fills in\n    // a real clock after mount (suppressHydrationWarning absorbs the swap).\n    const [now, setNow] = React.useState<number | null>(null)\n\n    // Keep the latest onComplete without re-arming the interval each render.\n    const onCompleteRef = React.useRef(onComplete)\n    React.useEffect(() => {\n      onCompleteRef.current = onComplete\n    })\n\n    // Which deadline onComplete has already fired for. Kept outside the effect\n    // so re-arming the timer (a changed `interval`) cannot fire it a second\n    // time for a target that already elapsed, while a new `to` still can.\n    const firedForRef = React.useRef<number | null>(null)\n\n    React.useEffect(() => {\n      if (!valid) return\n      const fireIfDone = (current: number) => {\n        if (current < targetMs) return false\n        if (firedForRef.current !== targetMs) {\n          firedForRef.current = targetMs\n          onCompleteRef.current?.()\n        }\n        return true\n      }\n\n      setNow(Date.now())\n      if (fireIfDone(Date.now())) return\n\n      const id = setInterval(() => {\n        const current = Date.now()\n        setNow(current)\n        if (fireIfDone(current)) clearInterval(id)\n      }, Math.max(50, interval))\n      return () => clearInterval(id)\n    }, [targetMs, valid, interval])\n\n    if (!valid) return null\n\n    // Before mount, fall back to a render-time clock so SSR output is sensible.\n    const parts = partsFrom(targetMs, now ?? Date.now())\n\n    return (\n      <div\n        ref={ref}\n        role=\"timer\"\n        aria-atomic=\"true\"\n        suppressHydrationWarning\n        className={cn(\n          \"inline-flex items-center gap-2 tabular-nums\",\n          className\n        )}\n        {...props}\n      >\n        <span className=\"sr-only\">{spoken(parts)}</span>\n        <span aria-hidden=\"true\" className=\"contents\">\n          {children\n            ? children(parts)\n            : parts.isComplete && completedLabel != null\n              ? completedLabel\n              : renderSegments(parts, showDays)}\n        </span>\n      </div>\n    )\n  }\n)\n\nconst SEGMENT_CLASS =\n  \"flex min-w-[2.5rem] flex-col items-center rounded-md border bg-card px-2 py-1.5 leading-none\"\n\nfunction Segment({ value, label }: { value: number; label: string }) {\n  return (\n    <span className={SEGMENT_CLASS}>\n      <span className=\"text-xl font-semibold text-foreground\">{pad(value)}</span>\n      <span className=\"mt-1 text-[0.625rem] font-medium uppercase tracking-wide text-muted-foreground\">\n        {label}\n      </span>\n    </span>\n  )\n}\n\nfunction renderSegments(parts: CountdownParts, showDays: boolean) {\n  const segments: { value: number; label: string }[] = []\n  if (showDays || parts.days > 0) {\n    segments.push({ value: parts.days, label: \"days\" })\n  }\n  segments.push(\n    { value: parts.hours, label: \"hrs\" },\n    { value: parts.minutes, label: \"min\" },\n    { value: parts.seconds, label: \"sec\" }\n  )\n  return (\n    <span className=\"inline-flex items-center gap-1.5\">\n      {segments.map((s) => (\n        <Segment key={s.label} value={s.value} label={s.label} />\n      ))}\n    </span>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}