{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "author": "AlgorizeTech",
  "description": "Scrollable hour / minute / AM-PM columns for choosing a time of day, with themed selection and disabled ranges. The building block behind the date pickers' time mode.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/TimePickerBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useRef } from \"react\";\nimport { getThemeStyles } from \"./date-picker-theme\";\nimport { setTime, type TimeFormat } from \"./time-picker-utils\";\n\nexport type { TimeFormat };\n\nexport interface TimePickerProps {\n  /** The date whose time is being edited. The day part is preserved on change. */\n  value: Date | null;\n  /** Receives a copy of `value` with the new hours / minutes applied. */\n  onChange: (next: Date) => void;\n  /** 12-hour clock with an AM / PM column, or a 24-hour one. Default \"12h\". */\n  format?: TimeFormat;\n  /** Minute granularity. Default 5. */\n  minuteStep?: number;\n  /** Times earlier than this are disabled (compared on the same day as `value`). */\n  minTime?: Date | null;\n  /** Times later than this are disabled (compared on the same day as `value`). */\n  maxTime?: Date | null;\n  /** Theme colour — same names / hex codes as the date pickers. */\n  color?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n\n// One scrollable column of options. Keeps the selected item centred so the\n// picker opens on the current value instead of at the top of the list.\nfunction Column<T extends string | number>({\n  items,\n  selected,\n  isDisabled,\n  onSelect,\n  render,\n  theme,\n  disabled,\n  label,\n}: {\n  items: T[];\n  selected: T | null;\n  isDisabled: (item: T) => boolean;\n  onSelect: (item: T) => void;\n  render: (item: T) => string;\n  theme: Record<string, string>;\n  disabled: boolean;\n  label: string;\n}) {\n  const listRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const list = listRef.current;\n    if (!list || selected === null) return;\n    const el = list.querySelector<HTMLElement>(`[data-value=\"${selected}\"]`);\n    if (!el) return;\n    // Scroll the column itself, never the page.\n    list.scrollTop = el.offsetTop - list.clientHeight / 2 + el.clientHeight / 2;\n  }, [selected]);\n\n  return (\n    <div\n      ref={listRef}\n      role=\"listbox\"\n      aria-label={label}\n      // shrink-0: a tight parent must never squeeze a column narrower than its\n      // buttons — that is exactly what produces a horizontal scrollbar here.\n      className=\"relative h-full shrink-0 overflow-y-auto overflow-x-hidden overscroll-contain px-0.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n    >\n      <div className=\"flex flex-col gap-0.5 py-1\">\n        {items.map((item) => {\n          const active = item === selected;\n          const off = disabled || isDisabled(item);\n          return (\n            <button\n              key={item}\n              type=\"button\"\n              role=\"option\"\n              aria-selected={active}\n              data-value={item}\n              disabled={off}\n              onClick={(e) => {\n                e.stopPropagation();\n                onSelect(item);\n              }}\n              className={cn(\n                \"w-10 h-7 shrink-0 rounded-md text-xs font-semibold tabular-nums transition-colors\",\n                active\n                  ? cn(theme.bg, \"shadow-sm\")\n                  : \"text-foreground hover:bg-muted\",\n                off && \"opacity-30 cursor-not-allowed hover:bg-transparent\",\n              )}\n            >\n              {render(item)}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport function TimePicker({\n  value,\n  onChange,\n  format = \"12h\",\n  minuteStep = 5,\n  minTime,\n  maxTime,\n  color = \"primary\",\n  disabled = false,\n  className,\n}: TimePickerProps) {\n  const { theme, style } = getThemeStyles(color);\n  const is12h = format === \"12h\";\n\n  const hours24 = value ? value.getHours() : null;\n  const minutes = value ? value.getMinutes() : null;\n  const period: \"AM\" | \"PM\" | null = hours24 === null ? null : hours24 >= 12 ? \"PM\" : \"AM\";\n  const hour12 = hours24 === null ? null : hours24 % 12 === 0 ? 12 : hours24 % 12;\n\n  const hourItems = is12h\n    ? Array.from({ length: 12 }, (_, i) => i + 1)\n    : Array.from({ length: 24 }, (_, i) => i);\n  const minuteItems = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);\n\n  const to24 = (h: number, p: \"AM\" | \"PM\" | null) => {\n    if (!is12h) return h;\n    const base = h % 12;\n    return (p ?? \"AM\") === \"PM\" ? base + 12 : base;\n  };\n\n  // A candidate time is out of bounds if it falls before minTime or after\n  // maxTime on the same day. Without a value there is no day to compare on,\n  // so nothing is disabled.\n  const outOfBounds = (h24: number, m: number) => {\n    if (!value) return false;\n    const t = setTime(value, h24, m).getTime();\n    if (minTime && t < minTime.getTime()) return true;\n    if (maxTime && t > maxTime.getTime()) return true;\n    return false;\n  };\n  const hourDisabled = (h: number, p = period) => {\n    const h24 = to24(h, p);\n    return minuteItems.every((m) => outOfBounds(h24, m));\n  };\n  const minuteDisabled = (m: number) =>\n    hours24 === null ? false : outOfBounds(hours24, m);\n  const periodDisabled = (p: \"AM\" | \"PM\") => hourItems.every((h) => hourDisabled(h, p));\n\n  const commit = (h24: number, m: number) => {\n    if (!value) return;\n    onChange(setTime(value, h24, m));\n  };\n\n  // When the current minute becomes invalid for a newly chosen hour (or\n  // period), snap to the first minute that is allowed in it.\n  const firstValidMinute = (h24: number) =>\n    minuteItems.find((m) => !outOfBounds(h24, m)) ?? minuteItems[0];\n\n  return (\n    <div\n      className={cn(\"flex items-stretch justify-center gap-1 h-32 min-w-max\", className)}\n      style={style}\n      aria-disabled={disabled}\n    >\n      <Column\n        label=\"Hour\"\n        items={hourItems}\n        selected={is12h ? hour12 : hours24}\n        isDisabled={(h) => hourDisabled(h)}\n        onSelect={(h) => {\n          const h24 = to24(h, period);\n          const m = minutes ?? 0;\n          commit(h24, outOfBounds(h24, m) ? firstValidMinute(h24) : m);\n        }}\n        render={(h) => pad(h)}\n        theme={theme}\n        disabled={disabled || !value}\n      />\n      <span className=\"self-center text-sm font-semibold text-muted-foreground\">:</span>\n      <Column\n        label=\"Minute\"\n        items={minuteItems}\n        selected={minutes !== null ? minutes - (minutes % minuteStep) : null}\n        isDisabled={minuteDisabled}\n        onSelect={(m) => commit(hours24 ?? 0, m)}\n        render={(m) => pad(m)}\n        theme={theme}\n        disabled={disabled || !value}\n      />\n      {is12h && (\n        <Column<\"AM\" | \"PM\">\n          label=\"AM or PM\"\n          items={[\"AM\", \"PM\"]}\n          selected={period}\n          isDisabled={periodDisabled}\n          onSelect={(p) => {\n            const h24 = to24(hour12 ?? 12, p);\n            const m = minutes ?? 0;\n            commit(h24, outOfBounds(h24, m) ? firstValidMinute(h24) : m);\n          }}\n          render={(p) => p}\n          theme={theme}\n          disabled={disabled || !value}\n        />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/TimePickerBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/date-picker-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    ring: \"focus-within:ring-primary/30 focus-within:border-primary/30\",\n    text: \"text-primary\",\n    bg: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n    bgSoft:\n      \"bg-primary/10 text-foreground hover:bg-primary/20\",\n    glow: \"focus-within:shadow-[0_0_20px_var(--primary)] focus-within:border-primary/30\",\n  },\n  indigo: {\n    ring: \"focus-within:ring-indigo-500/30 focus-within:border-indigo-500\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    bg: \"bg-indigo-600 text-white hover:bg-indigo-700\",\n    bgSoft:\n      \"bg-indigo-50 dark:bg-indigo-500/10 text-indigo-900 dark:text-indigo-100 hover:bg-indigo-100 dark:hover:bg-indigo-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(99,102,241,0.4)] focus-within:border-indigo-500\",\n  },\n  rose: {\n    ring: \"focus-within:ring-rose-500/30 focus-within:border-rose-500\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    bg: \"bg-rose-600 text-white hover:bg-rose-700\",\n    bgSoft:\n      \"bg-rose-50 dark:bg-rose-500/10 text-rose-900 dark:text-rose-100 hover:bg-rose-100 dark:hover:bg-rose-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(244,63,94,0.4)] focus-within:border-rose-500\",\n  },\n  emerald: {\n    ring: \"focus-within:ring-emerald-500/30 focus-within:border-emerald-500\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    bg: \"bg-emerald-600 text-white hover:bg-emerald-700\",\n    bgSoft:\n      \"bg-emerald-50 dark:bg-emerald-500/10 text-emerald-900 dark:text-emerald-100 hover:bg-emerald-100 dark:hover:bg-emerald-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(16,185,129,0.4)] focus-within:border-emerald-500\",\n  },\n  zinc: {\n    ring: \"focus-within:ring-zinc-500/30 focus-within:border-zinc-500 dark:focus-within:ring-white/20 dark:focus-within:border-white\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    bg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200\",\n    bgSoft:\n      \"bg-zinc-100 dark:bg-zinc-800/50 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-800\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(255,255,255,0.15)] focus-within:border-zinc-900 dark:focus-within:border-white\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string) => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        ring: \"focus-within:ring-[var(--theme-color)]/30 focus-within:border-[var(--theme-color)]\",\n        text: \"text-[var(--theme-color)]\",\n        bg: \"bg-[var(--theme-color)] text-white hover:opacity-90\",\n        bgSoft:\n          \"bg-[rgba(var(--theme-color-rgb),0.1)] text-[var(--theme-color)] hover:bg-[rgba(var(--theme-color-rgb),0.2)]\",\n        glow: \"focus-within:shadow-[0_0_20px_rgba(var(--theme-color-rgb),0.5)] focus-within:border-[var(--theme-color)]\",\n      },\n      style: {\n        \"--theme-color\": color,\n        \"--theme-color-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/date-picker-theme.ts"
    },
    {
      "path": "src/components/custom/blocks/time-picker-utils.ts",
      "content": "export type TimeFormat = \"12h\" | \"24h\";\n\n/** A new Date on the same day as `base` at the given time. */\nexport const setTime = (base: Date, hours: number, minutes: number) => {\n  const next = new Date(base);\n  next.setHours(hours, minutes, 0, 0);\n  return next;\n};\n\n/** Rounds `date` up to the next multiple of `step` minutes — \"now\" for a picker default. */\nexport const roundToStep = (date: Date, step: number) => {\n  const next = new Date(date);\n  const m = Math.ceil(next.getMinutes() / step) * step;\n  next.setMinutes(m, 0, 0);\n  return next;\n};\n\nexport const formatTime = (date: Date, format: TimeFormat = \"12h\") =>\n  new Intl.DateTimeFormat(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    hour12: format === \"12h\",\n  }).format(date);\n",
      "type": "registry:component",
      "target": "components/custom/blocks/time-picker-utils.ts"
    }
  ],
  "docs": "Named export: TimePicker. The setTime, roundToStep and formatTime helpers live in time-picker-utils.ts, which installs alongside. `value` is a Date whose day is preserved on change; `format` is 12h | 24h, `minuteStep` sets granularity, and `minTime` / `maxTime` disable times outside a window on that day. Used by `date-picker` and `range-date-picker` when `showTime` is on.",
  "categories": [
    "dates",
    "forms"
  ],
  "type": "registry:component"
}
