{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "description": "Text entry for a time, in 12- or 24-hour mode.",
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/field.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/time-picker.tsx",
      "content": "import { useId, useRef, useState, type ComponentProps, type KeyboardEvent } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Field, FieldDescription, FieldError, FieldLabel } from \"./field\"\nimport { isValidTime } from \"./time-picker-utils\"\n\ntype TimeValue = { hour: number; minute: number }\ntype TimeMode = \"12-hour\" | \"24-hour\"\n\ntype TimePickerProps = Omit<ComponentProps<\"div\">, \"onChange\"> & {\n  disabled?: boolean\n  error?: string\n  invalid?: boolean\n  label: string\n  max?: TimeValue\n  min?: TimeValue\n  mode?: TimeMode\n  onValueChange: (value: TimeValue) => void\n  readOnly?: boolean\n  supportingText?: string\n  value: TimeValue\n}\n\nfunction TimePicker({\n  className,\n  disabled = false,\n  error,\n  invalid = false,\n  label,\n  max,\n  min,\n  mode = \"12-hour\",\n  onValueChange,\n  readOnly = false,\n  supportingText,\n  value,\n  ...props\n}: TimePickerProps) {\n  const key = `${value.hour}:${value.minute}:${mode}`\n  const displayHour = mode === \"12-hour\" && value.hour >= 0 && value.hour <= 23\n    ? value.hour % 12 || 12\n    : value.hour\n  const [draft, setDraft] = useState(() => ({ key, hour: String(displayHour), minute: String(value.minute).padStart(2, \"0\") }))\n  if (draft.key !== key) {\n    setDraft({ key, hour: String(displayHour), minute: String(value.minute).padStart(2, \"0\") })\n  }\n  const hourRef = useRef<HTMLInputElement>(null)\n  const minuteRef = useRef<HTMLInputElement>(null)\n  const id = useId()\n  const valueInvalid = !isValidTime(value, min, max)\n  const emptySegment = draft.hour === \"\" || draft.minute === \"\"\n  const ariaInvalid = invalid || valueInvalid || emptySegment\n\n  const updateHour = (raw: string) => {\n    if (raw === \"\") return setDraft({ ...draft, hour: raw })\n    const entered = Number(raw)\n    let hour = entered\n    if (mode === \"12-hour\" && entered >= 1 && entered <= 12) {\n      const pm = value.hour >= 12\n      hour = (entered % 12) + (pm ? 12 : 0)\n    }\n    const next = { ...value, hour }\n    setDraft({ key: `${next.hour}:${next.minute}:${mode}`, hour: raw, minute: draft.minute })\n    onValueChange(next)\n  }\n\n  const updateMinute = (raw: string) => {\n    if (raw === \"\") return setDraft({ ...draft, minute: raw })\n    const next = { ...value, minute: Number(raw) }\n    setDraft({ key: `${next.hour}:${next.minute}:${mode}`, hour: draft.hour, minute: raw })\n    onValueChange(next)\n  }\n\n  const handleSegmentKey = (\n    segment: \"hour\" | \"minute\",\n    event: KeyboardEvent<HTMLInputElement>,\n  ) => {\n    if (event.key === \"ArrowRight\" && segment === \"hour\") {\n      event.preventDefault()\n      minuteRef.current?.focus()\n      return\n    }\n    if (event.key === \"ArrowLeft\" && segment === \"minute\") {\n      event.preventDefault()\n      hourRef.current?.focus()\n      return\n    }\n    if (readOnly) return\n    if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return\n    event.preventDefault()\n    const delta = event.key === \"ArrowUp\" ? 1 : -1\n    if (segment === \"hour\") {\n      const nextHour = (value.hour + delta + 24) % 24\n      onValueChange({ ...value, hour: nextHour })\n    } else {\n      const nextMinute = (value.minute + delta + 60) % 60\n      onValueChange({ ...value, minute: nextMinute })\n    }\n  }\n\n  const segmentClass = cn(\n    // Kit keyboard segment: 96 × 72, 8dp corners, Surface Container Highest,\n    // no stroke, display-medium numerals; the focused segment turns Primary\n    // Container with a 2dp Primary outline (painted inset so nothing reflows).\n    \"h-18 w-24 rounded-m3-sm bg-m3-surface-container-highest text-center text-m3-display-md text-foreground outline-none\",\n    \"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\",\n    \"transition-[background-color,box-shadow] duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast)\",\n    \"focus:bg-m3-primary-container focus:text-m3-on-primary-container focus:shadow-[inset_0_0_0_2px_var(--m3-primary)]\",\n    \"focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-m3-secondary\",\n    \"disabled:cursor-not-allowed disabled:text-muted-foreground/38\",\n    \"aria-invalid:shadow-[inset_0_0_0_2px_var(--m3-error)]\",\n  )\n\n  return (\n    <Field\n      {...props}\n      data-slot=\"time-picker\"\n      data-invalid={ariaInvalid || undefined}\n      data-disabled={disabled || undefined}\n      className={cn(\"max-w-[360px]\", className)}\n    >\n      <FieldLabel id={`${id}-label`}>{label}</FieldLabel>\n      <div role=\"group\" aria-labelledby={`${id}-label`} className=\"flex items-center gap-0\">\n        <input\n          ref={hourRef}\n          type=\"number\"\n          inputMode=\"numeric\"\n          aria-label=\"Hours\"\n          aria-invalid={ariaInvalid || undefined}\n          value={draft.hour}\n          min={mode === \"12-hour\" ? 1 : 0}\n          max={mode === \"12-hour\" ? 12 : 23}\n          disabled={disabled}\n          readOnly={readOnly}\n          className={segmentClass}\n          onChange={(event) => updateHour(event.target.value)}\n          onKeyDown={(event) => handleSegmentKey(\"hour\", event)}\n        />\n        <span aria-hidden=\"true\" className=\"flex w-6 justify-center text-m3-display-lg\">:</span>\n        <input\n          ref={minuteRef}\n          type=\"number\"\n          inputMode=\"numeric\"\n          aria-label=\"Minutes\"\n          aria-invalid={ariaInvalid || undefined}\n          value={draft.minute}\n          min={0}\n          max={59}\n          disabled={disabled}\n          readOnly={readOnly}\n          className={segmentClass}\n          onChange={(event) => updateMinute(event.target.value)}\n          onKeyDown={(event) => handleSegmentKey(\"minute\", event)}\n        />\n        {mode === \"12-hour\" && (\n          <select\n            aria-label=\"Period\"\n            value={value.hour >= 12 ? \"PM\" : \"AM\"}\n            disabled={disabled}\n            aria-readonly={readOnly || undefined}\n            // Kit period selector: 52 × 72, 8dp corners, 1dp Outline, title-medium.\n            className=\"ml-3 h-18 w-13 rounded-m3-sm border border-m3-outline bg-transparent text-center text-m3-title-md outline-none focus-visible:ring-3 focus-visible:ring-m3-secondary disabled:text-muted-foreground/38\"\n            onChange={(event) => {\n              if (readOnly) return\n              const pm = event.target.value === \"PM\"\n              onValueChange({ ...value, hour: (value.hour % 12) + (pm ? 12 : 0) })\n            }}\n          >\n            <option>AM</option>\n            <option>PM</option>\n          </select>\n        )}\n      </div>\n      {supportingText && <FieldDescription>{supportingText}</FieldDescription>}\n      {(error || valueInvalid || emptySegment) && (\n        <FieldError>\n          {error ?? (emptySegment ? \"Enter both hours and minutes\" : \"Enter a time within the allowed range\")}\n        </FieldError>\n      )}\n    </Field>\n  )\n}\n\nexport {\n  TimePicker,\n  type TimeMode,\n  type TimePickerProps,\n  type TimeValue,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/time-picker-utils.ts",
      "content": "import type { TimeMode, TimeValue } from \"./time-picker\"\n\nfunction formatTime(value: TimeValue, mode: TimeMode = \"24-hour\") {\n  if (mode === \"24-hour\") {\n    return `${String(value.hour).padStart(2, \"0\")}:${String(value.minute).padStart(2, \"0\")}`\n  }\n  const period = value.hour >= 12 ? \"PM\" : \"AM\"\n  const hours = value.hour % 12 || 12\n  return `${hours}:${String(value.minute).padStart(2, \"0\")} ${period}`\n}\n\nfunction parseTime(text: string, mode: TimeMode = \"24-hour\"): TimeValue | null {\n  const match = text.trim().match(/^(\\d{1,2}):(\\d{2})(?:\\s*(AM|PM))?$/i)\n  if (!match) return null\n  let hour = Number(match[1])\n  const minute = Number(match[2])\n  if (mode === \"12-hour\") {\n    const period = match[3]?.toUpperCase()\n    if (!period || hour < 1 || hour > 12) return null\n    hour = (hour % 12) + (period === \"PM\" ? 12 : 0)\n  }\n  if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null\n  return { hour, minute }\n}\n\nfunction minutesOf(value: TimeValue) {\n  return value.hour * 60 + value.minute\n}\n\nfunction isValidTime(value: TimeValue, min?: TimeValue, max?: TimeValue) {\n  if (value.hour < 0 || value.hour > 23 || value.minute < 0 || value.minute > 59) return false\n  const total = minutesOf(value)\n  return (!min || total >= minutesOf(min)) && (!max || total <= minutesOf(max))\n}\n\nexport { formatTime, isValidTime, parseTime }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}