{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "announcement-countdown",
  "type": "registry:component",
  "title": "Dispatch cutoff countdown",
  "description": "Live countdown to a same-day dispatch cutoff with flipping digits.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "announcement-bar",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json"
  ],
  "files": [
    {
      "path": "announcement-bar/announcement-countdown.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Truck } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\n\n/* Shipping cutoff bar. Counts down to `cutoffHour` (local time, 24h) on\n   business days and swaps to a next-day line after it. Each digit flips on\n   its own so only the changing character moves. Renders the static line on\n   the server and starts ticking after mount, so there is no hydration drift. */\n\nfunction remaining(now: Date, cutoffHour: number, businessDaysOnly: boolean) {\n  const day = now.getDay()\n  if (businessDaysOnly && (day === 0 || day === 6)) return null\n  const cutoff = new Date(now)\n  cutoff.setHours(cutoffHour, 0, 0, 0)\n  const ms = cutoff.getTime() - now.getTime()\n  if (ms <= 0) return null\n  const s = Math.floor(ms / 1000)\n  return { h: Math.floor(s / 3600), m: Math.floor((s % 3600) / 60), s: s % 60 }\n}\n\nfunction Digits({ value, reduce }: { value: string; reduce: boolean | null }) {\n  return (\n    <span className=\"inline-flex\">\n      {value.split(\"\").map((ch, i) => (\n        <span key={i} className=\"relative inline-block h-[1.25em] w-[1ch] overflow-hidden\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={ch}\n              initial={reduce ? { opacity: 0 } : { y: 8, opacity: 0 }}\n              animate={{ y: 0, opacity: 1 }}\n              exit={reduce ? { opacity: 0 } : { y: -8, opacity: 0 }}\n              transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}\n              className=\"absolute inset-0 text-center\"\n            >\n              {ch}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ))}\n    </span>\n  )\n}\n\nexport function AnnouncementCountdown({\n  cutoffHour = 14,\n  label = \"for same-day dispatch\",\n  afterCutoff = \"Ships next business day\",\n  showSeconds = true,\n  businessDaysOnly = true,\n  className,\n}: {\n  cutoffHour?: number\n  label?: string\n  afterCutoff?: string\n  showSeconds?: boolean\n  /** When true, Saturday and Sunday always show the after-cutoff line. */\n  businessDaysOnly?: boolean\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const [now, setNow] = useState<Date | null>(null)\n\n  useEffect(() => {\n    setNow(new Date())\n    const id = window.setInterval(() => setNow(new Date()), 1000)\n    return () => window.clearInterval(id)\n  }, [])\n\n  const left = now ? remaining(now, cutoffHour, businessDaysOnly) : null\n  const pad = (n: number) => String(n).padStart(2, \"0\")\n\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"off\"\n      data-sk-section=\"announcement-bar\"\n      data-sk-variant=\"announcement-countdown\"\n      className={cn(\"flex min-h-10 items-center justify-center gap-2 bg-sk-bar px-4 py-2 text-[13px] leading-5 text-sk-bar-ink\", className)}\n    >\n      <Truck size={14} aria-hidden className=\"shrink-0 opacity-70\" />\n      {left ? (\n        <p className=\"flex min-w-0 flex-wrap items-center justify-center gap-x-1.5 text-center\">\n          <span className=\"whitespace-nowrap\">Order in</span>\n          <span className=\"inline-flex items-center gap-1 font-sk-mono font-semibold tabular-nums\" aria-label={`${left.h} hours ${left.m} minutes`}>\n            <span className=\"inline-flex\" aria-hidden><Digits value={String(left.h)} reduce={reduce} />h</span>\n            <span className=\"inline-flex\" aria-hidden><Digits value={pad(left.m)} reduce={reduce} />m</span>\n            {showSeconds ? <span className=\"inline-flex\" aria-hidden><Digits value={pad(left.s)} reduce={reduce} />s</span> : null}\n          </span>\n          <span className=\"text-balance\">{label}</span>\n        </p>\n      ) : (\n        <p className={cn(\"text-balance text-center transition-opacity duration-300\", now ? \"opacity-100\" : \"opacity-0\")}>{afterCutoff}</p>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/announcement-bar/announcement-countdown.tsx"
    }
  ],
  "docs": "cutoffHour is local time in 24h. Weekends and post-cutoff show the next-day line.",
  "meta": {
    "section": "announcement-bar",
    "variant": "countdown",
    "niche": [
      "any"
    ],
    "commerce": [
      "shipping",
      "urgency"
    ],
    "previewHeight": 160
  }
}