{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "countdown-timer",
  "title": "Countdown Timer",
  "description": "A countdown timer that counts down to a duration or an absolute end date, with flip-cell digit transitions, a compact text mode, and an optional loop.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/countdown-timer/countdown-timer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport type CountdownCellShape = \"rounded\" | \"square\" | \"circle\";\nexport type CountdownAlign = \"left\" | \"center\" | \"right\";\nexport type CountdownLabelPosition = \"top\" | \"bottom\";\n\nexport interface CountdownTimerProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** ISO date/time to count down to, e.g. \"2027-12-31T23:59:00\". Takes priority over `duration`. */\n  endDate?: string;\n  /** Duration in milliseconds to count down from, starting now. Ignored if `endDate` is set. */\n  duration?: number;\n  showDays?: boolean;\n  showHours?: boolean;\n  showSeconds?: boolean;\n  loop?: boolean;\n  expiredText?: string;\n  compact?: boolean;\n  cellSize?: number;\n  cellShape?: CountdownCellShape;\n  showLabel?: boolean;\n  labelPosition?: CountdownLabelPosition;\n  align?: CountdownAlign;\n  blinkColon?: boolean;\n  fontFamily?: \"system\" | \"mono\" | \"serif\";\n}\n\ninterface CountdownThemeColors {\n  digitColor: string;\n  cellBg: string;\n  cellBorderColor: string;\n  labelColor: string;\n  colonColor: string;\n}\n\ninterface TimeState {\n  days: number;\n  hours: number;\n  minutes: number;\n  seconds: number;\n  expired: boolean;\n}\n\nfunction calcTime(targetMs: number): TimeState {\n  const diff = targetMs - Date.now();\n  if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true };\n  return {\n    days: Math.floor(diff / 86400000),\n    hours: Math.floor((diff % 86400000) / 3600000),\n    minutes: Math.floor((diff % 3600000) / 60000),\n    seconds: Math.floor((diff % 60000) / 1000),\n    expired: false,\n  };\n}\n\nconst INITIAL_TIME: TimeState = { days: 0, hours: 0, minutes: 0, seconds: 0, expired: false };\n\nfunction useCountdown(targetMs: number): TimeState {\n  // Starts from a fixed placeholder (not calcTime(Date.now())) so the server-rendered\n  // markup and the client's first hydration pass always match — Date.now() differs\n  // between the two, which would otherwise throw a hydration mismatch.\n  const [time, setTime] = React.useState<TimeState>(INITIAL_TIME);\n  React.useEffect(() => {\n    setTime(calcTime(targetMs));\n    const id = setInterval(() => setTime(calcTime(targetMs)), 1000);\n    return () => clearInterval(id);\n  }, [targetMs]);\n  return time;\n}\n\nconst pad2 = (n: number) => String(n).padStart(2, \"0\");\n\nconst COUNTDOWN_FONT_MAP: Record<\"system\" | \"mono\" | \"serif\", string> = {\n  system: \"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n  mono: \"'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace\",\n  serif: \"Georgia, 'Times New Roman', serif\",\n};\n\nfunction FlipCell({\n  value,\n  label,\n  cellSize,\n  radius,\n  showLabel,\n  labelPosition,\n  colors,\n}: {\n  value: number;\n  label: string;\n  cellSize: number;\n  radius: number | string;\n  showLabel: boolean;\n  labelPosition: CountdownLabelPosition;\n  colors?: CountdownThemeColors;\n}) {\n  const digit = pad2(value);\n  const digitSize = Math.round(cellSize * 0.43);\n  const labelSize = Math.max(8, Math.round(digitSize * 0.52));\n\n  return (\n    <div className=\"relative\" style={{ width: cellSize, height: cellSize }}>\n      <div\n        className={cn(\"relative flex items-center justify-center overflow-hidden border\", !colors && \"border-white/10 bg-[#141414]\")}\n        style={{ width: cellSize, height: cellSize, borderRadius: radius, ...(colors && { borderColor: colors.cellBorderColor, background: colors.cellBg }) }}\n      >\n        <AnimatePresence mode=\"popLayout\">\n          <motion.span\n            key={digit}\n            initial={{ y: \"-100%\", opacity: 0 }}\n            animate={{ y: 0, opacity: 1 }}\n            exit={{ y: \"100%\", opacity: 0 }}\n            transition={{ duration: 0.28, ease: [0.32, 0, 0.67, 0] }}\n            className={cn(\"absolute font-extrabold leading-none tracking-tight tabular-nums\", !colors && \"text-[#f5f4f1]\")}\n            style={{ fontSize: digitSize, ...(colors && { color: colors.digitColor }) }}\n          >\n            {digit}\n          </motion.span>\n        </AnimatePresence>\n      </div>\n      {showLabel && (\n        <span\n          className={cn(\n            \"absolute left-1/2 -translate-x-1/2 whitespace-nowrap font-bold uppercase tracking-widest\",\n            !colors && \"text-[#f5f4f1]/40\",\n            labelPosition === \"top\" ? \"bottom-full mb-1.5\" : \"top-full mt-1.5\",\n          )}\n          style={{ fontSize: labelSize, ...(colors && { color: colors.labelColor }) }}\n        >\n          {label}\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction Colon({ size, blink, cellSize, color }: { size: number; blink: boolean; cellSize: number; color?: string }) {\n  const [visible, setVisible] = React.useState(true);\n  React.useEffect(() => {\n    if (!blink) return;\n    const id = setInterval(() => setVisible((v) => !v), 1000);\n    return () => clearInterval(id);\n  }, [blink]);\n\n  return (\n    <div className=\"flex items-center\" style={{ height: cellSize }}>\n      <span\n        className={cn(\"font-bold leading-none transition-opacity duration-300\", !color && \"text-[#f5f4f1]/70\")}\n        style={{ fontSize: size, opacity: blink ? (visible ? 0.7 : 0.18) : 0.7, ...(color && { color }) }}\n      >\n        :\n      </span>\n    </div>\n  );\n}\n\nexport function CountdownTimer({\n  endDate,\n  duration = 3 * 86400000,\n  showDays = true,\n  showHours = true,\n  showSeconds = true,\n  loop = false,\n  expiredText = \"Offer ended!\",\n  compact = false,\n  cellSize = 58,\n  cellShape = \"rounded\",\n  showLabel = true,\n  labelPosition = \"bottom\",\n  align = \"center\",\n  blinkColon = true,\n  fontFamily = \"system\",\n  className,\n  style,\n  ...props\n}: CountdownTimerProps) {\n  const [loopKey, setLoopKey] = React.useState(0);\n  const loopFired = React.useRef(false);\n\n  const targetMs = React.useMemo(() => {\n    if (endDate) {\n      const dt = new Date(endDate);\n      return Number.isNaN(dt.getTime()) ? Date.now() + duration : dt.getTime();\n    }\n    return Date.now() + duration;\n    // loopKey intentionally forces a fresh target when looping a duration countdown\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [endDate, duration, loopKey]);\n\n  const time = useCountdown(targetMs);\n\n  React.useEffect(() => {\n    if (time.expired && loop && !endDate) {\n      if (!loopFired.current) {\n        loopFired.current = true;\n        setLoopKey((k) => k + 1);\n      }\n    } else {\n      loopFired.current = false;\n    }\n  }, [time.expired, loop, endDate]);\n\n  const radius: number | string =\n    cellShape === \"circle\" ? \"50%\" : cellShape === \"square\" ? 4 : Math.round(cellSize * 0.175);\n  const justify = align === \"left\" ? \"justify-start\" : align === \"right\" ? \"justify-end\" : \"justify-center\";\n  const gap = Math.round(cellSize * 0.17);\n  const colonSize = Math.round(cellSize * 0.53);\n\n  if (time.expired && !loop) {\n    return (\n      <div className={cn(\"flex w-full flex-col items-center gap-3\", className)} style={{ fontFamily: COUNTDOWN_FONT_MAP[fontFamily], ...style }} {...props}>\n        <div className={cn(\"flex w-full items-center\", justify)}>\n          <span className=\"font-bold text-[#f5f4f1]\" style={{ fontSize: Math.round(cellSize * 0.43) }}>\n            {expiredText}\n          </span>\n        </div>\n      </div>\n    );\n  }\n\n  if (compact) {\n    const numSize = Math.round(cellSize * 0.7);\n    const unitSize = Math.round(cellSize * 0.33);\n    const sepSize = Math.round(cellSize * 0.6);\n    const unitMarginLeft = Math.round(cellSize * 0.05);\n    const unitPaddingBottom = Math.round(cellSize * 0.06);\n    return (\n      <div className={cn(\"flex w-full flex-col items-center gap-3\", className)} style={{ fontFamily: COUNTDOWN_FONT_MAP[fontFamily], ...style }} {...props}>\n        <div className={cn(\"flex w-full items-baseline\", justify)}>\n          {showDays && time.days > 0 && (\n            <>\n              <span className=\"font-extrabold leading-none tracking-tight tabular-nums text-[#f5f4f1]\" style={{ fontSize: numSize }}>\n                {pad2(time.days)}\n              </span>\n              {showLabel && <span className=\"self-end font-bold tracking-wider uppercase text-[#f5f4f1]/50\" style={{ fontSize: unitSize, marginLeft: unitMarginLeft, paddingBottom: unitPaddingBottom }}>d</span>}\n              <span className=\"font-bold leading-none text-[#f5f4f1]/45\" style={{ fontSize: sepSize, margin: `0 ${Math.round(cellSize * 0.06)}px` }}>:</span>\n            </>\n          )}\n          {showHours && (\n            <>\n              <span className=\"font-extrabold leading-none tracking-tight tabular-nums text-[#f5f4f1]\" style={{ fontSize: numSize }}>\n                {pad2(time.hours)}\n              </span>\n              {showLabel && <span className=\"self-end font-bold tracking-wider uppercase text-[#f5f4f1]/50\" style={{ fontSize: unitSize, marginLeft: unitMarginLeft, paddingBottom: unitPaddingBottom }}>h</span>}\n              <span className=\"font-bold leading-none text-[#f5f4f1]/45\" style={{ fontSize: sepSize, margin: `0 ${Math.round(cellSize * 0.06)}px` }}>:</span>\n            </>\n          )}\n          <span className=\"font-extrabold leading-none tracking-tight tabular-nums text-[#f5f4f1]\" style={{ fontSize: numSize }}>\n            {pad2(time.minutes)}\n          </span>\n          {showLabel && <span className=\"self-end font-bold tracking-wider uppercase text-[#f5f4f1]/50\" style={{ fontSize: unitSize, marginLeft: unitMarginLeft, paddingBottom: unitPaddingBottom }}>m</span>}\n          {showSeconds && (\n            <>\n              <span className=\"font-bold leading-none text-[#f5f4f1]/45\" style={{ fontSize: sepSize, margin: `0 ${Math.round(cellSize * 0.06)}px` }}>:</span>\n              <span className=\"font-extrabold leading-none tracking-tight tabular-nums text-[#f5f4f1]\" style={{ fontSize: numSize }}>\n                {pad2(time.seconds)}\n              </span>\n              {showLabel && <span className=\"self-end font-bold tracking-wider uppercase text-[#f5f4f1]/50\" style={{ fontSize: unitSize, marginLeft: unitMarginLeft, paddingBottom: unitPaddingBottom }}>s</span>}\n            </>\n          )}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"flex w-full flex-col items-center gap-3\", className)} style={{ fontFamily: COUNTDOWN_FONT_MAP[fontFamily], ...style }} {...props}>\n    <div className={cn(\"flex w-full items-center\", justify)} style={{ gap }}>\n      {showDays && time.days > 0 && (\n        <>\n          <FlipCell value={time.days} label=\"days\" cellSize={cellSize} radius={radius} showLabel={showLabel} labelPosition={labelPosition} />\n          <Colon size={colonSize} blink={blinkColon} cellSize={cellSize} />\n        </>\n      )}\n      {showHours && (\n        <>\n          <FlipCell value={time.hours} label=\"hrs\" cellSize={cellSize} radius={radius} showLabel={showLabel} labelPosition={labelPosition} />\n          <Colon size={colonSize} blink={blinkColon} cellSize={cellSize} />\n        </>\n      )}\n      <FlipCell value={time.minutes} label=\"min\" cellSize={cellSize} radius={radius} showLabel={showLabel} labelPosition={labelPosition} />\n      {showSeconds && (\n        <>\n          <Colon size={colonSize} blink={blinkColon} cellSize={cellSize} />\n          <FlipCell value={time.seconds} label=\"sec\" cellSize={cellSize} radius={radius} showLabel={showLabel} labelPosition={labelPosition} />\n        </>\n      )}\n    </div>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}