{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "author": "@fma-ds/ds",
  "description": "A composable time picker: hour, minute, second and AM/PM columns, in a popover or in place.\n\n- **Layouts**: in a popover (`TimePickerTrigger` + `TimePickerContent`), behind a typable field (`TimePickerInput`, which reads `930`, `14h30`, `2:30 pm`…) or in place (`TimePickerPanel`). `<TimePicker />` alone renders a trigger and a popover.\n- **Value**: `@internationalized/date` `Time` objects (`value`/`defaultValue`/`onChange`, `null` when empty).\n- **Columns**: `hourCycle`, `granularity` and `periodPosition` shape them.\n- **Choices**: `hourStep`/`minuteStep`/`secondStep`, `minValue`/`maxValue` (an overnight window when min > max) and `isTimeUnavailable` narrow them, and a pick moves to the nearest selectable time.\n- **`requireConfirm`**: keeps picks as a draft until OK.\n- **Keyboard**: each column is an `aria-activedescendant` listbox — arrows, Home/End, PageUp/PageDown and typed digits pick, Left/Right move between columns, Enter commits and closes.\n- **Forms**: `name` submits `HH:mm` (or `HH:mm:ss`) with a native form; `isRequired`, `isInvalid`, `isReadOnly` and `isDisabled` work as in react-aria.\n- **`useTimePicker()`**: exposes the state inside a picker.\n\nEvery string comes from `i18n`.",
  "dependencies": [
    "@internationalized/date@^3.12.4",
    "cn@^0.5.0",
    "lucide-react@^1.61.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/input-group",
    "@fma-ds/popover"
  ],
  "files": [
    {
      "path": "src/core/time-picker/time-picker.tsx",
      "content": "\"use client\";\n\nimport { Time } from \"@internationalized/date\";\nimport { cn } from \"cn\";\nimport { ClockIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Dialog } from \"react-aria-components\";\nimport { flushSync } from \"react-dom\";\n\nimport { Button } from \"@/core/button/button\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/core/input-group/input-group\";\nimport { Popover } from \"@/core/popover/popover\";\n\n/* -------------------------------------------------------------------------- */\n/*                                    Types                                   */\n/* -------------------------------------------------------------------------- */\n\n/** A time of day as plain numbers. `hour` runs from 0 to 23 whatever the hour cycle. */\ntype TimePickerTime = { hour: number; minute: number; second: number };\n\n/** The finest column, and the precision of the value the picker emits. */\ntype TimePickerGranularity = \"hour\" | \"minute\" | \"second\";\n\ntype TimePickerHourCycle = 12 | 24;\n\ntype TimePickerPeriod = \"am\" | \"pm\";\n\n/** Where AM/PM goes: after the time in English, before it in Chinese, Japanese and Korean. */\ntype TimePickerPeriodPosition = \"start\" | \"end\";\n\ntype TimePickerUnit = \"hour\" | \"minute\" | \"second\";\n\ntype TimePickerColumnType = TimePickerUnit | \"period\";\n\ntype TimePickerLabels = {\n  /** Trigger text while there is no value. */\n  placeholder: string;\n  /** Column headers, which also name the listboxes. */\n  hour: string;\n  minute: string;\n  second: string;\n  period: string;\n  am: string;\n  pm: string;\n  now: string;\n  clear: string;\n  confirm: string;\n  /** Names the popover and an inline panel. */\n  panelLabel: string;\n  /** The clock button in `TimePickerInput`. */\n  openPicker: string;\n};\n\ntype TimePickerFormatContext = {\n  hourCycle: TimePickerHourCycle;\n  granularity: TimePickerGranularity;\n  periodPosition: TimePickerPeriodPosition;\n  labels: TimePickerLabels;\n  /** The merged `formatSegment`, so an override reaches `formatValue` too. */\n  formatSegment: TimePickerFunctions[\"formatSegment\"];\n};\n\ntype TimePickerFunctions = {\n  /** Option text in the columns. In 12-hour time the hour arrives as 1 to 12. */\n  formatSegment: (value: number, unit: TimePickerUnit) => string;\n  /** The value as the trigger and the input show it. */\n  formatValue: (time: TimePickerTime, context: TimePickerFormatContext) => string;\n};\n\ntype TimePickerI18nConfig = {\n  labels: TimePickerLabels;\n  functions: TimePickerFunctions;\n};\n\n/** One level deeper than `Partial`, so a single label can be overridden. */\ntype TimePickerI18nOverrides = {\n  labels?: Partial<TimePickerLabels>;\n  functions?: Partial<TimePickerFunctions>;\n};\n\nconst DEFAULT_TIME_PICKER_I18N: TimePickerI18nConfig = {\n  labels: {\n    placeholder: \"Select time\",\n    hour: \"Hour\",\n    minute: \"Minute\",\n    second: \"Second\",\n    period: \"AM/PM\",\n    am: \"AM\",\n    pm: \"PM\",\n    now: \"Now\",\n    clear: \"Clear\",\n    confirm: \"OK\",\n    panelLabel: \"Choose time\",\n    openPicker: \"Open time picker\",\n  },\n  functions: {\n    formatSegment: (value) => String(value).padStart(2, \"0\"),\n    formatValue: (time, context) => {\n      const { hourCycle, granularity, periodPosition, labels } = context;\n      const hour = hourCycle === 12 ? time.hour % 12 || 12 : time.hour;\n      let text = `${context.formatSegment(hour, \"hour\")}:${context.formatSegment(time.minute, \"minute\")}`;\n      if (granularity === \"second\") {\n        text += `:${context.formatSegment(time.second, \"second\")}`;\n      }\n      if (hourCycle === 24) return text;\n      const period = time.hour < 12 ? labels.am : labels.pm;\n      return periodPosition === \"start\" ? `${period} ${text}` : `${text} ${period}`;\n    },\n  },\n};\n\n/** Merges per section, so an override can carry one key without restating the rest. */\nfunction mergeTimePickerI18n(overrides?: TimePickerI18nOverrides): TimePickerI18nConfig {\n  if (!overrides?.labels && !overrides?.functions) {\n    return DEFAULT_TIME_PICKER_I18N;\n  }\n  return {\n    labels: { ...DEFAULT_TIME_PICKER_I18N.labels, ...overrides.labels },\n    functions: {\n      ...DEFAULT_TIME_PICKER_I18N.functions,\n      ...overrides.functions,\n    },\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Values                                   */\n/* -------------------------------------------------------------------------- */\n\nconst pad = (value: number) => String(value).padStart(2, \"0\");\n\nconst toSeconds = (time: TimePickerTime) => time.hour * 3600 + time.minute * 60 + time.second;\n\nconst periodOf = (hour: number): TimePickerPeriod => (hour < 12 ? \"am\" : \"pm\");\n\nconst sameTime = (a: TimePickerTime | null, b: TimePickerTime | null) =>\n  a === b || (!!a && !!b && toSeconds(a) === toSeconds(b));\n\n/** A `Time` at the picker's precision: seconds drop below `second` granularity. */\nfunction fromTime(\n  time: Time | null | undefined,\n  granularity: TimePickerGranularity,\n): TimePickerTime | null {\n  if (!time) return null;\n  return {\n    hour: time.hour,\n    minute: time.minute,\n    second: granularity === \"second\" ? time.second : 0,\n  };\n}\n\nconst toTime = (time: TimePickerTime) => new Time(time.hour, time.minute, time.second);\n\n/** The format `<input type=\"time\">` submits: `HH:mm`, or `HH:mm:ss` at `second` granularity. */\nfunction formatFieldValue(time: TimePickerTime, granularity: TimePickerGranularity) {\n  const value = `${pad(time.hour)}:${pad(time.minute)}`;\n  return granularity === \"second\" ? `${value}:${pad(time.second)}` : value;\n}\n\n/* NFKC folds full-width digits and colons; Arabic-Indic and Persian digits\n   need their own map. */\nfunction normalizeTyped(text: string) {\n  return text\n    .normalize(\"NFKC\")\n    .replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 0x660))\n    .replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 0x6f0))\n    .replace(/[  ]/g, \" \")\n    .toLowerCase();\n}\n\nfunction parseTyped(\n  text: string,\n  options: {\n    hourCycle?: TimePickerHourCycle;\n    period?: TimePickerPeriod;\n    labels?: Pick<TimePickerLabels, \"am\" | \"pm\">;\n  },\n): TimePickerTime | null {\n  const labels = options.labels ?? DEFAULT_TIME_PICKER_I18N.labels;\n  let source = normalizeTyped(text).trim();\n  let period: TimePickerPeriod | undefined;\n  /* Localized labels first: a lone \"a\" would also match inside \"am\". */\n  for (const [label, value] of [\n    [labels.pm, \"pm\"],\n    [labels.am, \"am\"],\n  ] as const) {\n    const needle = normalizeTyped(label).trim();\n    if (needle && source.includes(needle)) {\n      period = value;\n      source = source.replace(needle, \" \").trim();\n      break;\n    }\n  }\n  if (!period) {\n    const suffix = /\\s*([ap])\\.?\\s*(?:m\\.?)?$/.exec(source);\n    if (suffix) {\n      period = suffix[1] === \"a\" ? \"am\" : \"pm\";\n      source = source.slice(0, suffix.index);\n    }\n  }\n  if (/[^\\d\\s:.h]/.test(source)) return null;\n  const parts = source.split(/[^\\d]+/).filter(Boolean);\n  let fields = parts;\n  if (parts.length === 1 && parts[0].length > 2) {\n    /* Packed digits: 930, 0930, 93015, 093015. */\n    const digits = parts[0];\n    if (digits.length > 6) return null;\n    const hourLength = digits.length % 2 === 1 ? 1 : 2;\n    fields = [digits.slice(0, hourLength)];\n    for (let index = hourLength; index < digits.length; index += 2) {\n      fields.push(digits.slice(index, index + 2));\n    }\n  }\n  if (fields.length === 0 || fields.length > 3) return null;\n  if (fields.slice(1).some((field) => field.length !== 2)) return null;\n  let hour = Number(fields[0]);\n  const minute = Number(fields[1] ?? 0);\n  const second = Number(fields[2] ?? 0);\n  if (minute > 59 || second > 59) return null;\n  const resolved =\n    period ??\n    (options.hourCycle === 12 && hour >= 1 && hour <= 12 ? (options.period ?? \"am\") : undefined);\n  if (resolved) {\n    if (hour < 1 || hour > 12) return null;\n    hour = (hour % 12) + (resolved === \"pm\" ? 12 : 0);\n  }\n  return hour < 24 ? { hour, minute, second } : null;\n}\n\n/**\n * Reads what a person types into `TimePickerInput`: `9`, `930`, `0930`,\n * `9:30`, `14.30`, `14h30`, `2:30 pm`, `2p`, a localized period on either\n * side, and full-width, Arabic-Indic or Persian digits. A 12-hour time typed\n * without a period takes `period`.\n */\nfunction parseTimeInput(\n  text: string,\n  options: {\n    hourCycle?: TimePickerHourCycle;\n    period?: TimePickerPeriod;\n    labels?: Pick<TimePickerLabels, \"am\" | \"pm\">;\n  } = {},\n): Time | null {\n  const time = parseTyped(text, options);\n  return time ? toTime(time) : null;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Grid                                    */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The selectable times: step-aligned values, a window and a predicate. Times\n * are walked by index, second fastest, so every search is bounded by the grid.\n */\ntype TimeGrid = {\n  hours: number[];\n  minutes: number[];\n  seconds: number[];\n  min: number | null;\n  max: number | null;\n  isTimeUnavailable?: (time: Time) => boolean;\n};\n\ntype TimeFilter = Partial<TimePickerTime> & { period?: TimePickerPeriod };\n\nfunction stepValues(step: number | undefined, limit: number) {\n  const size = Math.floor(step ?? 1);\n  if (!Number.isFinite(size)) return [0];\n  const values: number[] = [];\n  for (let value = 0; value < limit; value += Math.max(1, size)) {\n    values.push(value);\n  }\n  return values;\n}\n\nconst gridSize = (grid: TimeGrid) => grid.hours.length * grid.minutes.length * grid.seconds.length;\n\nfunction timeAt(grid: TimeGrid, index: number): TimePickerTime {\n  const seconds = grid.seconds.length;\n  const minutes = grid.minutes.length;\n  return {\n    hour: grid.hours[Math.floor(index / (minutes * seconds))],\n    minute: grid.minutes[Math.floor(index / seconds) % minutes],\n    second: grid.seconds[index % seconds],\n  };\n}\n\nfunction lastAtOrBelow(values: number[], limit: number) {\n  let found = 0;\n  for (let index = 0; index < values.length; index++) {\n    if (values[index] <= limit) found = index;\n  }\n  return found;\n}\n\n/** The latest grid time at or before `seconds`. Always exists: 00:00:00 is on the grid. */\nfunction floorIndex(grid: TimeGrid, seconds: number) {\n  const hour = lastAtOrBelow(grid.hours, seconds / 3600);\n  const inHour = seconds - grid.hours[hour] * 3600;\n  const minute = lastAtOrBelow(grid.minutes, inHour / 60);\n  const second = lastAtOrBelow(grid.seconds, inHour - grid.minutes[minute] * 60);\n  return (hour * grid.minutes.length + minute) * grid.seconds.length + second;\n}\n\n/* `minValue` after `maxValue` is an overnight window, 22:00 to 02:00. */\nfunction spanInWindow(grid: TimeGrid, from: number, to: number) {\n  const { min, max } = grid;\n  if (min !== null && max !== null && min > max) return to >= min || from <= max;\n  return (min === null || to >= min) && (max === null || from <= max);\n}\n\nfunction isEnabled(grid: TimeGrid, time: TimePickerTime) {\n  const seconds = toSeconds(time);\n  return spanInWindow(grid, seconds, seconds) && !grid.isTimeUnavailable?.(toTime(time));\n}\n\nfunction matches(time: TimePickerTime, filter: TimeFilter) {\n  return (\n    (filter.hour === undefined || time.hour === filter.hour) &&\n    (filter.minute === undefined || time.minute === filter.minute) &&\n    (filter.second === undefined || time.second === filter.second) &&\n    (filter.period === undefined || periodOf(time.hour) === filter.period)\n  );\n}\n\n/** Whether any enabled time matches `filter`. Hours outside the window cost O(1). */\nfunction hasEnabled(grid: TimeGrid, filter: TimeFilter) {\n  for (const hour of grid.hours) {\n    if (filter.hour !== undefined && hour !== filter.hour) continue;\n    if (filter.period !== undefined && periodOf(hour) !== filter.period) continue;\n    if (!spanInWindow(grid, hour * 3600, hour * 3600 + 3599)) continue;\n    for (const minute of grid.minutes) {\n      if (filter.minute !== undefined && minute !== filter.minute) continue;\n      for (const second of grid.seconds) {\n        if (filter.second !== undefined && second !== filter.second) continue;\n        if (isEnabled(grid, { hour, minute, second })) return true;\n      }\n    }\n  }\n  return false;\n}\n\n/**\n * The enabled time closest to `target` among those matching `filter`,\n * walking outwards from it. A tie goes to the earlier time.\n */\nfunction nearestEnabled(\n  grid: TimeGrid,\n  target: TimePickerTime,\n  filter: TimeFilter = {},\n): TimePickerTime | null {\n  const goal = toSeconds(target);\n  const total = gridSize(grid);\n  let back = floorIndex(grid, goal);\n  let ahead = back + 1;\n  while (back >= 0 || ahead < total) {\n    const behind = back >= 0 ? timeAt(grid, back) : null;\n    const next = ahead < total ? timeAt(grid, ahead) : null;\n    const useBack = !!behind && (!next || goal - toSeconds(behind) <= toSeconds(next) - goal);\n    const time = useBack ? behind : next;\n    if (time && matches(time, filter) && isEnabled(grid, time)) return time;\n    if (useBack) back--;\n    else ahead++;\n  }\n  return null;\n}\n\n/** The first enabled time from `start` in `direction`, optionally wrapping round the day. */\nfunction walkEnabled(\n  grid: TimeGrid,\n  start: number,\n  direction: 1 | -1,\n  wrap: boolean,\n): TimePickerTime | null {\n  const total = gridSize(grid);\n  let index = start;\n  for (let step = 0; step < total; step++) {\n    if (index < 0 || index >= total) {\n      if (!wrap) return null;\n      index = (index + total) % total;\n    }\n    const time = timeAt(grid, index);\n    if (isEnabled(grid, time)) return time;\n    index += direction;\n  }\n  return null;\n}\n\n/** The next enabled time after `from`, or before it with -1, wrapping at midnight. */\nfunction stepEnabled(grid: TimeGrid, from: TimePickerTime, direction: 1 | -1) {\n  const index = floorIndex(grid, toSeconds(from));\n  const onGrid = sameTime(timeAt(grid, index), from);\n  const start = direction === 1 ? index + 1 : onGrid ? index - 1 : index;\n  return walkEnabled(grid, start, direction, true);\n}\n\n/** The earliest enabled time, counted from `minValue` so a night shift starts at 22:00. */\nfunction firstEnabledTime(grid: TimeGrid) {\n  if (grid.min === null) return walkEnabled(grid, 0, 1, true);\n  const index = floorIndex(grid, grid.min);\n  const start = toSeconds(timeAt(grid, index)) < grid.min ? index + 1 : index;\n  return walkEnabled(grid, start, 1, true);\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Context                                  */\n/* -------------------------------------------------------------------------- */\n\ntype ColumnGoal = { goal: TimePickerTime; result: TimePickerTime };\n\ntype TimePickerContextValue = {\n  id: string;\n  /** The committed value. */\n  time: TimePickerTime | null;\n  /** What the columns show: a pending draft under `requireConfirm`, else the value. */\n  display: TimePickerTime | null;\n  grid: TimeGrid;\n  firstEnabled: TimePickerTime | null;\n  hourCycle: TimePickerHourCycle;\n  granularity: TimePickerGranularity;\n  periodPosition: TimePickerPeriodPosition;\n  i18n: TimePickerI18nConfig;\n  isDisabled: boolean;\n  isReadOnly: boolean;\n  isInvalid: boolean;\n  isRequired: boolean;\n  requireConfirm: boolean;\n  fade: boolean;\n  label: string | undefined;\n  labelledBy: string | undefined;\n  describedBy: string | undefined;\n  isOpen: boolean;\n  setOpen: (open: boolean) => void;\n  /** What the popover anchors to: the trigger, or the input's clock button. */\n  triggerRef: React.RefObject<HTMLElement | null>;\n  /** Commits, or writes the draft under `requireConfirm`. */\n  select: (time: TimePickerTime | null) => void;\n  /** Commits and bypasses the draft. */\n  commit: (time: TimePickerTime | null) => void;\n  confirm: () => void;\n  hasDraft: boolean;\n  discardDraft: () => void;\n  format: (time: TimePickerTime) => string;\n  /** What a column pick aimed at, kept while the value is the time it produced. */\n  getGoal: () => ColumnGoal | null;\n  setGoal: (goal: ColumnGoal) => void;\n  /** The trigger or the input: where a failed `isRequired` check sends focus. */\n  registerFocusTarget: (node: HTMLElement | null) => void;\n  registerColumns: (node: HTMLElement | null) => void;\n  getColumns: () => HTMLElement | null;\n};\n\nconst TimePickerContext = React.createContext<TimePickerContextValue | null>(null);\n\n/** \"inline\" unless a part renders inside `TimePickerContent`. */\nconst SurfaceContext = React.createContext<\"inline\" | \"popup\">(\"inline\");\n\nfunction useTimePickerContext(part: string) {\n  const context = React.useContext(TimePickerContext);\n  if (!context) throw new Error(`${part} must be used within a TimePicker`);\n  return context;\n}\n\ntype TimePickerApi = {\n  /** The committed value, or `null`. */\n  value: Time | null;\n  isOpen: boolean;\n  setOpen: (open: boolean) => void;\n  /** Commits a value as given, with no snapping and no draft. */\n  setValue: (value: Time | null) => void;\n  /** Same as `TimePickerNow`. */\n  now: () => void;\n  /** Same as `TimePickerClear`. */\n  clear: () => void;\n  /** Commits a pending draft and closes the popover. */\n  confirm: () => void;\n};\n\n/** The picker's state and actions, inside a `TimePicker`. */\nfunction useTimePicker(): TimePickerApi {\n  const context = useTimePickerContext(\"useTimePicker\");\n  return {\n    value: context.time ? toTime(context.time) : null,\n    isOpen: context.isOpen,\n    setOpen: context.setOpen,\n    setValue: (value) => context.commit(fromTime(value, context.granularity)),\n    now: () => pickNow(context),\n    clear: () => context.select(null),\n    confirm: context.confirm,\n  };\n}\n\n/* The only clock read, inside an action: reading it while rendering would\n   differ between the server and the client. It floors to the steps, so 14:38\n   on a 15 minute step is 14:30, then looks forward before back. */\nfunction pickNow(context: TimePickerContextValue) {\n  const now = new Date();\n  const { grid } = context;\n  const seconds =\n    now.getHours() * 3600 +\n    (context.granularity === \"hour\" ? 0 : now.getMinutes() * 60) +\n    (context.granularity === \"second\" ? now.getSeconds() : 0);\n  const index = floorIndex(grid, seconds);\n  const time = walkEnabled(grid, index, 1, false) ?? walkEnabled(grid, index - 1, -1, false);\n  if (time) context.select(time);\n}\n\nfunction assignRef<T>(ref: React.Ref<T> | undefined, node: T | null) {\n  if (typeof ref === \"function\") ref(node);\n  else if (ref) ref.current = node;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Root                                    */\n/* -------------------------------------------------------------------------- */\n\ntype TimePickerProps = {\n  /** The selected time (controlled). `null` is empty. */\n  value?: Time | null;\n  /** The initial time (uncontrolled), and what a form reset restores. */\n  defaultValue?: Time | null;\n  /** Fires with the new time, or `null` when cleared. */\n  onChange?: (value: Time | null) => void;\n  /** Whether the popover is open (controlled). */\n  isOpen?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n  /** 12 shows hours 12, 01 to 11 and adds the AM/PM column. Defaults to 24. */\n  hourCycle?: TimePickerHourCycle;\n  /** The finest column, and the precision of the value. Defaults to \"minute\". */\n  granularity?: TimePickerGranularity;\n  /** Where AM/PM goes, in the text and in the columns. Defaults to \"end\". */\n  periodPosition?: TimePickerPeriodPosition;\n  /** Hours offered, counted from 0 on the 24-hour scale. */\n  hourStep?: number;\n  /** Minutes offered, counted from 0. */\n  minuteStep?: number;\n  /** Seconds offered, counted from 0. */\n  secondStep?: number;\n  /** Earliest selectable time, inclusive. After `maxValue`, the window runs overnight. */\n  minValue?: Time;\n  /** Latest selectable time, inclusive. */\n  maxValue?: Time;\n  /** Marks single times unavailable. Pass a stable function; it runs per candidate time. */\n  isTimeUnavailable?: (time: Time) => boolean;\n  /** Picks, Now and Clear stay a draft until OK; closing discards them. */\n  requireConfirm?: boolean;\n  /** Fades the top and bottom of a column while more rows are scrolled out of view there. */\n  fade?: boolean;\n  isDisabled?: boolean;\n  /** The popover opens and the columns take focus, but nothing changes the value. */\n  isReadOnly?: boolean;\n  isInvalid?: boolean;\n  /** Fails native form validation while empty. */\n  isRequired?: boolean;\n  /** Submits the value with a native form under this name, as `HH:mm` or `HH:mm:ss`. */\n  name?: string;\n  form?: string;\n  /** The hidden form field, for form libraries that focus a field on error. */\n  inputRef?: React.Ref<HTMLInputElement>;\n  /** Lands on the trigger or the input. */\n  id?: string;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  \"aria-describedby\"?: string;\n  i18n?: TimePickerI18nOverrides;\n  /** Without children the picker renders a trigger and a popover; these reach the trigger. */\n  placeholder?: string;\n  className?: string;\n  children?: React.ReactNode;\n};\n\n/**\n * Holds the value, the popover state and any pending draft, and renders no\n * element. Without children it renders a `TimePickerTrigger` and a\n * `TimePickerContent`, so `<TimePicker />` alone is a complete picker.\n */\nfunction TimePicker({\n  value,\n  defaultValue = null,\n  onChange,\n  isOpen: isOpenProp,\n  defaultOpen = false,\n  onOpenChange,\n  hourCycle = 24,\n  granularity = \"minute\",\n  periodPosition = \"end\",\n  hourStep,\n  minuteStep,\n  secondStep,\n  minValue,\n  maxValue,\n  isTimeUnavailable,\n  requireConfirm = false,\n  fade = true,\n  isDisabled = false,\n  isReadOnly = false,\n  isInvalid = false,\n  isRequired = false,\n  name,\n  form,\n  inputRef,\n  id: idProp,\n  \"aria-label\": label,\n  \"aria-labelledby\": labelledBy,\n  \"aria-describedby\": describedBy,\n  i18n: i18nProp,\n  placeholder,\n  className,\n  children,\n}: TimePickerProps) {\n  const generatedId = React.useId();\n  const id = idProp ?? generatedId;\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue);\n  /* What a form reset restores, as a native field returns to its default. */\n  const [initialValue] = React.useState(defaultValue);\n  const [openState, setOpenState] = React.useState(defaultOpen);\n  const [draft, setDraft] = React.useState<{ time: TimePickerTime | null } | null>(null);\n  const isOpen = isOpenProp ?? openState;\n  /* Every open and close drops the draft, a parent's included. */\n  const [seenOpen, setSeenOpen] = React.useState(isOpen);\n  if (seenOpen !== isOpen) {\n    setSeenOpen(isOpen);\n    setDraft(null);\n  }\n  const goal = React.useRef<ColumnGoal | null>(null);\n  const focusTarget = React.useRef<HTMLElement | null>(null);\n  const triggerRef = React.useRef<HTMLElement | null>(null);\n  const columns = React.useRef<HTMLElement | null>(null);\n  const fieldRef = React.useRef<HTMLInputElement | null>(null);\n  const resetRef = React.useRef(() => {});\n\n  const time = fromTime(value !== undefined ? value : uncontrolled, granularity);\n  const i18n = React.useMemo(() => mergeTimePickerI18n(i18nProp), [i18nProp]);\n  const min = minValue ? toSeconds(minValue) : null;\n  const max = maxValue ? toSeconds(maxValue) : null;\n  const grid = React.useMemo<TimeGrid>(\n    () => ({\n      hours: stepValues(hourStep, 24),\n      minutes: granularity === \"hour\" ? [0] : stepValues(minuteStep, 60),\n      seconds: granularity === \"second\" ? stepValues(secondStep, 60) : [0],\n      min,\n      max,\n      isTimeUnavailable,\n    }),\n    [hourStep, minuteStep, secondStep, granularity, min, max, isTimeUnavailable],\n  );\n  const firstEnabled = React.useMemo(() => firstEnabledTime(grid), [grid]);\n\n  const setOpen = (next: boolean) => {\n    if (next && isDisabled) return;\n    if (isOpenProp === undefined) setOpenState(next);\n    onOpenChange?.(next);\n  };\n\n  const commit = (next: TimePickerTime | null) => {\n    setDraft(null);\n    if (sameTime(next, time)) return;\n    const nextValue = next ? toTime(next) : null;\n    if (value === undefined) setUncontrolled(nextValue);\n    onChange?.(nextValue);\n  };\n\n  const context: TimePickerContextValue = {\n    id,\n    time,\n    display: draft ? draft.time : time,\n    grid,\n    firstEnabled,\n    hourCycle,\n    granularity,\n    periodPosition,\n    i18n,\n    isDisabled,\n    isReadOnly,\n    isInvalid,\n    isRequired,\n    requireConfirm,\n    fade,\n    label,\n    labelledBy,\n    describedBy,\n    isOpen,\n    setOpen,\n    triggerRef,\n    select: (next) => {\n      if (isDisabled || isReadOnly) return;\n      if (requireConfirm) setDraft({ time: next });\n      else commit(next);\n    },\n    commit,\n    confirm: () => {\n      if (draft) commit(draft.time);\n      if (isOpen) setOpen(false);\n    },\n    hasDraft: draft !== null,\n    discardDraft: () => setDraft(null),\n    format: (next) =>\n      i18n.functions.formatValue(next, {\n        hourCycle,\n        granularity,\n        periodPosition,\n        labels: i18n.labels,\n        formatSegment: i18n.functions.formatSegment,\n      }),\n    getGoal: () => goal.current,\n    setGoal: (next) => {\n      goal.current = next;\n    },\n    registerFocusTarget: (node) => {\n      if (node) focusTarget.current = node;\n    },\n    registerColumns: (node) => {\n      columns.current = node;\n    },\n    getColumns: () => columns.current,\n  };\n\n  React.useEffect(() => {\n    resetRef.current = () => commit(fromTime(initialValue, granularity));\n  });\n\n  /* Listens on the field's own root and asks for its form at reset time, so a\n     `form` attribute is honoured. The restore waits a task, like a native\n     reset's default action, because any listener may still cancel it. */\n  const hasField = !!name || isRequired;\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-attach when the field mounts or unmounts\n  React.useEffect(() => {\n    const root = fieldRef.current?.getRootNode();\n    if (!root) return;\n    let pending = 0;\n    const onReset = (event: Event) => {\n      if (event.target !== fieldRef.current?.form) return;\n      pending = window.setTimeout(() => {\n        if (!event.defaultPrevented) resetRef.current();\n      });\n    };\n    root.addEventListener(\"reset\", onReset, true);\n    return () => {\n      root.removeEventListener(\"reset\", onReset, true);\n      window.clearTimeout(pending);\n    };\n  }, [hasField]);\n\n  return (\n    <TimePickerContext.Provider value={context}>\n      {children === undefined ? (\n        <>\n          <TimePickerTrigger className={className} placeholder={placeholder} />\n          <TimePickerContent />\n        </>\n      ) : (\n        children\n      )}\n      {hasField && (\n        <input\n          ref={(node) => {\n            fieldRef.current = node;\n            assignRef(inputRef, node);\n          }}\n          type=\"text\"\n          name={name}\n          form={form}\n          required={isRequired}\n          disabled={isDisabled}\n          value={time ? formatFieldValue(time, granularity) : \"\"}\n          onChange={() => {}}\n          tabIndex={-1}\n          aria-hidden=\"true\"\n          data-slot=\"time-picker-field\"\n          /* A failed `isRequired` check focuses this field; hand that to the control. */\n          onFocus={(event) => {\n            const target =\n              focusTarget.current ?? columns.current?.querySelector<HTMLElement>(LIST_SELECTOR);\n            if (target) target.focus();\n            else event.currentTarget.blur();\n          }}\n          className=\"pointer-events-none absolute size-px max-w-px opacity-0\"\n        />\n      )}\n    </TimePickerContext.Provider>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Triggers                                  */\n/* -------------------------------------------------------------------------- */\n\ntype TimePickerValueProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  placeholder?: React.ReactNode;\n};\n\n/** The committed value as text, or the placeholder. For custom trigger content. */\nfunction TimePickerValue({ placeholder, className, ...props }: TimePickerValueProps) {\n  const context = useTimePickerContext(\"TimePickerValue\");\n  return (\n    <span\n      id={`${context.id}-value`}\n      data-slot=\"time-picker-value\"\n      data-placeholder={context.time ? undefined : \"\"}\n      className={cn(\"truncate\", className)}\n      {...props}\n    >\n      {context.time\n        ? context.format(context.time)\n        : (placeholder ?? context.i18n.labels.placeholder)}\n    </span>\n  );\n}\n\nconst TRIGGER_CLASS =\n  \"justify-between font-normal tabular-nums data-placeholder:text-muted-foreground\";\n\ntype TimePickerTriggerProps = Omit<React.ComponentProps<typeof Button>, \"children\"> & {\n  placeholder?: React.ReactNode;\n  /** Replaces the value and the clock icon. */\n  children?: React.ReactNode;\n};\n\n/** A Button that opens the picker. Children replace the value and icon. */\nfunction TimePickerTrigger({\n  className,\n  variant = \"outline\",\n  placeholder,\n  children,\n  ref,\n  onPress,\n  ...props\n}: TimePickerTriggerProps) {\n  const context = useTimePickerContext(\"TimePickerTrigger\");\n  const labelledBy = props[\"aria-labelledby\"] ?? context.labelledBy;\n  return (\n    <Button\n      variant={variant}\n      isDisabled={context.isDisabled}\n      aria-label={context.label}\n      aria-describedby={context.describedBy}\n      aria-haspopup=\"dialog\"\n      aria-expanded={context.isOpen}\n      aria-invalid={context.isInvalid || undefined}\n      data-invalid={context.isInvalid ? \"\" : undefined}\n      {...props}\n      /* The value stays in the name: a label alone replaces a button's text. */\n      aria-labelledby={labelledBy ? `${labelledBy} ${context.id}-value` : undefined}\n      id={context.id}\n      ref={(node: HTMLButtonElement | null) => {\n        context.triggerRef.current = node;\n        context.registerFocusTarget(node);\n        assignRef(ref, node);\n      }}\n      onPress={(event) => {\n        onPress?.(event);\n        context.setOpen(!context.isOpen);\n      }}\n      data-slot=\"time-picker-trigger\"\n      data-placeholder={context.time ? undefined : \"\"}\n      className={cn(TRIGGER_CLASS, className)}\n    >\n      {children === undefined ? (\n        <>\n          <TimePickerValue placeholder={placeholder} />\n          <ClockIcon data-icon=\"inline-end\" aria-hidden=\"true\" className=\"text-muted-foreground\" />\n        </>\n      ) : (\n        children\n      )}\n    </Button>\n  );\n}\n\ntype TimePickerInputProps = Omit<\n  React.ComponentProps<typeof InputGroupInput>,\n  \"value\" | \"defaultValue\"\n>;\n\n/**\n * A typable field with a clock button that opens the picker. Text is read on\n * blur and Enter; ArrowUp and ArrowDown step to the next selectable time and\n * Alt+ArrowDown opens the popover.\n */\nfunction TimePickerInput({\n  className,\n  placeholder,\n  onBlur,\n  onKeyDown,\n  onChange,\n  ref,\n  ...props\n}: TimePickerInputProps) {\n  const context = useTimePickerContext(\"TimePickerInput\");\n  const { grid, time } = context;\n  /* `null` while the field shows the value rather than an edit in progress. */\n  const [text, setText] = React.useState<string | null>(null);\n  const shown = text ?? (time ? context.format(time) : \"\");\n  const mask =\n    context.hourCycle === 12\n      ? context.periodPosition === \"start\"\n        ? \"-- --:--\"\n        : \"--:-- --\"\n      : context.granularity === \"second\"\n        ? \"--:--:--\"\n        : \"--:--\";\n\n  const parse = (source: string) => {\n    if (context.hourCycle === 24) {\n      return parseTyped(source, { labels: context.i18n.labels });\n    }\n    /* No period typed: prefer the one on screen, but not over a time that\n       only the other half of the day allows (2:30 with a morning minimum). */\n    const period = time\n      ? periodOf(time.hour)\n      : context.firstEnabled\n        ? periodOf(context.firstEnabled.hour)\n        : \"am\";\n    const options = { hourCycle: 12 as const, labels: context.i18n.labels };\n    const here = parseTyped(source, { ...options, period });\n    const there = parseTyped(source, {\n      ...options,\n      period: period === \"am\" ? \"pm\" : \"am\",\n    });\n    if (!here || !there || sameTime(here, there)) return here;\n    const inBounds = (typed: TimePickerTime) =>\n      spanInWindow(grid, toSeconds(typed), toSeconds(typed));\n    if (inBounds(here)) return here;\n    return inBounds(there) ? there : here;\n  };\n\n  const read = () => {\n    if (text === null) return;\n    setText(null);\n    if (!text.trim()) return context.commit(null);\n    /* A custom formatValue may not parse back; unchanged text is no edit. */\n    if (text === (time ? context.format(time) : \"\")) return;\n    const typed = parse(text);\n    if (!typed) return;\n    const truncated = {\n      hour: typed.hour,\n      minute: context.granularity === \"hour\" ? 0 : typed.minute,\n      second: context.granularity === \"second\" ? typed.second : 0,\n    };\n    /* Snaps to the steps as well as the bounds: an enabled time on the grid\n       is its own nearest. */\n    const next = nearestEnabled(grid, truncated);\n    if (next) context.commit(next);\n  };\n\n  return (\n    <InputGroup\n      data-slot=\"time-picker-input\"\n      data-disabled={context.isDisabled ? \"\" : undefined}\n      className={className}\n    >\n      <InputGroupInput\n        value={shown}\n        placeholder={placeholder ?? mask}\n        disabled={context.isDisabled}\n        readOnly={context.isReadOnly}\n        required={context.isRequired}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck={false}\n        aria-keyshortcuts=\"Alt+ArrowDown\"\n        aria-label={context.label}\n        aria-labelledby={context.labelledBy}\n        aria-describedby={context.describedBy}\n        aria-invalid={context.isInvalid || undefined}\n        className=\"tabular-nums\"\n        onChange={(event) => {\n          onChange?.(event);\n          setText(event.target.value);\n        }}\n        onBlur={(event) => {\n          onBlur?.(event);\n          read();\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented) return;\n          if (event.key === \"Enter\") {\n            /* Flushed, so an implicit form submit reads the new value. */\n            flushSync(() => read());\n          } else if (event.key === \"Escape\" && text !== null) {\n            event.preventDefault();\n            setText(null);\n          } else if (event.altKey && event.key === \"ArrowDown\") {\n            event.preventDefault();\n            read();\n            context.setOpen(true);\n          } else if (\n            (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") &&\n            !event.altKey &&\n            !context.isReadOnly\n          ) {\n            event.preventDefault();\n            const direction = event.key === \"ArrowUp\" ? 1 : -1;\n            const from = (text !== null && parse(text)) || time;\n            const next = from\n              ? stepEnabled(grid, from, direction)\n              : direction === 1\n                ? context.firstEnabled\n                : walkEnabled(grid, gridSize(grid) - 1, -1, true);\n            setText(null);\n            if (next) context.commit(next);\n          }\n        }}\n        {...props}\n        id={context.id}\n        ref={(node: HTMLInputElement | null) => {\n          context.registerFocusTarget(node);\n          assignRef(ref, node);\n        }}\n      />\n      <InputGroupAddon align=\"inline-end\">\n        <InputGroupButton\n          size=\"icon-xs\"\n          isDisabled={context.isDisabled}\n          aria-label={context.i18n.labels.openPicker}\n          aria-haspopup=\"dialog\"\n          aria-expanded={context.isOpen}\n          data-slot=\"time-picker-input-trigger\"\n          ref={(node: HTMLButtonElement | null) => {\n            context.triggerRef.current = node;\n          }}\n          onPress={() => context.setOpen(!context.isOpen)}\n        >\n          <ClockIcon aria-hidden=\"true\" />\n        </InputGroupButton>\n      </InputGroupAddon>\n    </InputGroup>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Panel                                   */\n/* -------------------------------------------------------------------------- */\n\ntype TimePickerContentProps = Omit<\n  React.ComponentProps<typeof Popover>,\n  \"children\" | \"isOpen\" | \"onOpenChange\" | \"triggerRef\"\n> & {\n  /** Replaces the columns and the footer. */\n  children?: React.ReactNode;\n};\n\n/**\n * The popover: columns and footer by default. A modal react-aria `Popover`\n * around a `Dialog`, so Tab cycles inside it, Escape and an outside press\n * close it, and focus returns to the trigger. The first column takes focus on\n * open.\n */\nfunction TimePickerContent({\n  className,\n  placement = \"bottom start\",\n  children,\n  ...props\n}: TimePickerContentProps) {\n  const context = useTimePickerContext(\"TimePickerContent\");\n  return (\n    <Popover\n      placement={placement}\n      triggerRef={context.triggerRef}\n      isOpen={context.isOpen}\n      onOpenChange={context.setOpen}\n      className={cn(\"w-auto gap-0 overflow-hidden p-0\", className)}\n      {...props}\n    >\n      <Dialog\n        aria-labelledby={context.labelledBy}\n        aria-label={\n          context.labelledBy ? undefined : (context.label ?? context.i18n.labels.panelLabel)\n        }\n        className=\"outline-none\"\n      >\n        <SurfaceContext.Provider value=\"popup\">\n          <FocusFirstColumn />\n          {children === undefined ? (\n            <>\n              <TimePickerColumns />\n              <TimePickerFooter />\n            </>\n          ) : (\n            children\n          )}\n        </SurfaceContext.Provider>\n      </Dialog>\n    </Popover>\n  );\n}\n\n/* Once per open. Child effects run before the Dialog's own, which then sees\n   focus inside and leaves it on the column. */\nfunction FocusFirstColumn() {\n  const context = useTimePickerContext(\"TimePickerContent\");\n  const getColumns = React.useRef(context.getColumns);\n  React.useEffect(() => {\n    getColumns.current()?.querySelector<HTMLElement>(LIST_SELECTOR)?.focus({ preventScroll: true });\n  }, []);\n  return null;\n}\n\ntype TimePickerPanelProps = React.ComponentProps<\"div\">;\n\n/**\n * The columns and the footer, rendered in place: put it straight inside a\n * `TimePicker`, with no trigger or content, for an always-open picker.\n */\nfunction TimePickerPanel({ className, children, ...props }: TimePickerPanelProps) {\n  const context = useTimePickerContext(\"TimePickerPanel\");\n  const inline = React.useContext(SurfaceContext) === \"inline\";\n  return (\n    <div\n      {...(inline && {\n        role: \"group\",\n        \"aria-labelledby\": context.labelledBy,\n        \"aria-label\": context.labelledBy\n          ? undefined\n          : (context.label ?? context.i18n.labels.panelLabel),\n      })}\n      data-slot=\"time-picker-panel\"\n      data-disabled={context.isDisabled ? \"\" : undefined}\n      className={cn(\"flex w-fit flex-col\", className)}\n      {...props}\n    >\n      {children === undefined ? (\n        <>\n          <TimePickerColumns />\n          <TimePickerFooter />\n        </>\n      ) : (\n        children\n      )}\n    </div>\n  );\n}\n\ntype TimePickerColumnsProps = React.ComponentProps<\"div\">;\n\n/**\n * The row of columns: one per unit the picker needs by default. Always left\n * to right, because a time reads hour first in right-to-left text too.\n */\nfunction TimePickerColumns({ className, children, ref, ...props }: TimePickerColumnsProps) {\n  const context = useTimePickerContext(\"TimePickerColumns\");\n  const { display, granularity } = context;\n  const period = context.hourCycle === 12 && <TimePickerColumn type=\"period\" />;\n  return (\n    <div\n      dir=\"ltr\"\n      data-slot=\"time-picker-columns\"\n      className={cn(\"flex divide-x\", className)}\n      {...props}\n      ref={(node) => {\n        context.registerColumns(node);\n        assignRef(ref, node);\n      }}\n    >\n      <span id={`${context.id}-current`} hidden>\n        {display ? context.format(display) : context.i18n.labels.placeholder}\n      </span>\n      {children === undefined ? (\n        <>\n          {context.periodPosition === \"start\" && period}\n          <TimePickerColumn type=\"hour\" />\n          {granularity !== \"hour\" && <TimePickerColumn type=\"minute\" />}\n          {granularity === \"second\" && <TimePickerColumn type=\"second\" />}\n          {context.periodPosition === \"end\" && period}\n        </>\n      ) : (\n        children\n      )}\n    </div>\n  );\n}\n\nconst LIST_SELECTOR = '[data-slot=\"time-picker-column-list\"]:not([aria-disabled=\"true\"])';\n\n/* Row height and list padding follow the Select item, as custom properties\n   so a list is exactly N rows tall. */\nconst COLUMN_CLASS =\n  \"group/time-picker-column flex min-w-16 flex-1 flex-col [--time-picker-option-height:calc(var(--spacing)*7)] [--time-picker-list-padding:calc(var(--spacing)*1)]\";\n\n/* Gains contrast while its list has keyboard focus, which says which column\n   the arrows move. */\nconst LABEL_CLASS =\n  \"px-1.5 pt-2 pb-1 text-center text-xs whitespace-nowrap text-muted-foreground transition-colors select-none group-has-focus-visible/time-picker-column:text-foreground\";\n\n/* Five rows unless an ancestor sets `--time-picker-rows`; keep it odd so the\n   selection can sit in the middle. The fade masks only an end with rows\n   scrolled past it, so the first and last rows are never dimmed at rest;\n   `--time-picker-fade-size` sets its depth. */\nconst LIST_CLASS =\n  \"group/time-picker-list relative flex h-[calc(var(--time-picker-option-height)*var(--time-picker-rows,5)+var(--spacing)*(var(--time-picker-rows,5)-1)+var(--time-picker-list-padding)*2)] flex-col gap-1 overflow-y-auto overscroll-contain px-2 py-(--time-picker-list-padding) outline-none [scrollbar-width:none] data-empty:focus-visible:outline-2 data-empty:focus-visible:-outline-offset-2 data-empty:focus-visible:outline-ring data-empty:focus-visible:outline-solid data-fade:[mask-image:linear-gradient(to_bottom,transparent,#000_var(--time-picker-fade-start,0px),#000_calc(100%-var(--time-picker-fade-end,0px)),transparent)] data-overflow-start:[--time-picker-fade-start:var(--time-picker-fade-size,var(--time-picker-option-height))] data-overflow-end:[--time-picker-fade-end:var(--time-picker-fade-size,var(--time-picker-option-height))] [&::-webkit-scrollbar]:hidden\";\n\n/* The filled pill marks the value; hover uses accent, and the keyboard row\n   takes the Button's focus ring, which the list's padding leaves room for. */\nconst OPTION_CLASS =\n  \"flex h-(--time-picker-option-height) shrink-0 cursor-default items-center justify-center rounded-md text-sm tabular-nums ring-ring/50 transition-[color,background-color,box-shadow] duration-100 select-none not-aria-selected:hover:bg-accent not-aria-selected:hover:text-accent-foreground aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-selected:bg-primary aria-selected:font-medium aria-selected:text-primary-foreground group-focus-visible/time-picker-list:data-highlighted:ring-3 forced-colors:aria-selected:bg-[Highlight] forced-colors:aria-selected:text-[HighlightText] forced-colors:group-focus-visible/time-picker-list:data-highlighted:outline-2 forced-colors:group-focus-visible/time-picker-list:data-highlighted:outline-solid\";\n\n/* Marks which ends of a list have rows scrolled out of view, for the fade.\n   Written to the DOM directly: it changes every scroll frame and nothing\n   renders from it. */\nfunction syncOverflow(list: HTMLElement) {\n  list.toggleAttribute(\"data-overflow-start\", list.scrollTop > 1);\n  list.toggleAttribute(\n    \"data-overflow-end\",\n    list.scrollTop + list.clientHeight < list.scrollHeight - 1,\n  );\n}\n\ntype TimePickerOption = {\n  value: number | TimePickerPeriod;\n  label: string;\n  /** The number typed to reach it: 12-hour labels, not 24-hour values. */\n  typed: number | null;\n  disabled: boolean;\n};\n\ntype TimePickerColumnProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  type: TimePickerColumnType;\n  /** Header text. Defaults to the label for the type. */\n  label?: React.ReactNode;\n};\n\n/**\n * One unit as a listbox. Selection follows the keyboard, like a native wheel.\n * Renders nothing for a unit the granularity or the hour cycle leaves out.\n */\nfunction TimePickerColumn({ type, label, className, ...props }: TimePickerColumnProps) {\n  const context = useTimePickerContext(\"TimePickerColumn\");\n  const { display, grid, hourCycle, i18n, firstEnabled } = context;\n  const baseId = React.useId();\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const typeahead = React.useRef({ text: \"\", at: 0 });\n\n  const listPeriod: TimePickerPeriod = display\n    ? periodOf(display.hour)\n    : firstEnabled\n      ? periodOf(firstEnabled.hour)\n      : \"am\";\n  const displayHour = display?.hour;\n  const displayMinute = display?.minute;\n  /* Availability is the expensive part; labels are recomputed freely. */\n  const flags = React.useMemo(() => {\n    if (type === \"period\") {\n      return [hasEnabled(grid, { period: \"am\" }), hasEnabled(grid, { period: \"pm\" })];\n    }\n    if (type === \"hour\") {\n      return grid.hours.map((hour) => hasEnabled(grid, { hour }));\n    }\n    const values = type === \"minute\" ? grid.minutes : grid.seconds;\n    return values.map((value) =>\n      hasEnabled(grid, {\n        hour: displayHour,\n        minute: type === \"second\" ? displayMinute : value,\n        second: type === \"second\" ? value : undefined,\n      }),\n    );\n  }, [type, grid, displayHour, displayMinute]);\n\n  const options = columnOptions(type, grid, flags, hourCycle, listPeriod, i18n);\n  const current = display\n    ? type === \"period\"\n      ? periodOf(display.hour)\n      : display[type]\n    : undefined;\n  const selectedIndex = options.findIndex((option) => option.value === current);\n  const enabled = options.flatMap((option, index) => (option.disabled ? [] : [index]));\n  const highlightIndex = highlightFor(\n    options,\n    enabled,\n    selectedIndex,\n    current,\n    firstEnabled && !display ? columnValueOf(firstEnabled, type) : undefined,\n  );\n  const highlighted = highlightIndex === -1 ? undefined : options[highlightIndex];\n  const optionId = (option: TimePickerOption) => `${baseId}-${option.value}`;\n  const highlightedId = highlighted ? optionId(highlighted) : undefined;\n  const interactive = !context.isDisabled;\n  const editable = interactive && !context.isReadOnly;\n\n  /* Centers the highlighted row, instantly, whenever it changes, and again\n     when the list resizes: rows take their height from the theme, which can\n     settle after mount. `offsetTop` ignores the popover's zoom-in transform;\n     `scrollIntoView` would not, and would scroll the page too. */\n  React.useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!list) return;\n    const center = () => {\n      const option = highlightedId ? list.ownerDocument.getElementById(highlightedId) : null;\n      if (option) {\n        list.scrollTop = option.offsetTop - (list.clientHeight - option.offsetHeight) / 2;\n      }\n      syncOverflow(list);\n    };\n    center();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(center);\n    observer.observe(list);\n    return () => observer.disconnect();\n  }, [highlightedId]);\n\n  const hidden =\n    (type === \"period\" && hourCycle !== 12) ||\n    (type === \"minute\" && context.granularity === \"hour\") ||\n    (type === \"second\" && context.granularity !== \"second\");\n  if (hidden) return null;\n\n  const pick = (option: TimePickerOption) => {\n    if (option.disabled || !editable) return;\n    /* Aim from the last goal while the value is still what it produced, so\n       09:15 through a clamped 08:30 comes back to 09:15, not 09:30. */\n    const kept = context.getGoal();\n    const base =\n      kept && sameTime(kept.result, display)\n        ? kept.goal\n        : (display ?? { hour: 0, minute: 0, second: 0 });\n    const target =\n      option.value === \"am\" || option.value === \"pm\"\n        ? { ...base, hour: (base.hour % 12) + (option.value === \"pm\" ? 12 : 0) }\n        : { ...base, [type]: option.value };\n    const filter: TimeFilter =\n      type === \"period\"\n        ? { period: option.value as TimePickerPeriod }\n        : { [type]: option.value as number };\n    const time = nearestEnabled(grid, target, filter);\n    if (!time) return;\n    context.setGoal({ goal: target, result: time });\n    context.select(time);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (event.ctrlKey || event.metaKey || event.nativeEvent.isComposing) return;\n    if (event.altKey) {\n      if (event.key === \"ArrowUp\" && context.isOpen) {\n        event.preventDefault();\n        context.confirm();\n      }\n      return;\n    }\n    /* Where the arrows start: the selected row, between rows for a value\n       off the steps, and just short of the highlight when empty. */\n    const down = event.key === \"ArrowDown\" || event.key === \"PageDown\";\n    let position = selectedIndex;\n    if (position === -1) {\n      const above =\n        typeof current === \"number\"\n          ? options.findIndex(\n              (option) => typeof option.value === \"number\" && option.value > current,\n            )\n          : -1;\n      position =\n        typeof current === \"number\"\n          ? (above === -1 ? options.length : above) - 0.5\n          : highlightIndex === -1\n            ? -0.5\n            : highlightIndex + (down ? -0.5 : 0.5);\n    }\n    const move = (index: number | undefined) => {\n      event.preventDefault();\n      if (index !== undefined) pick(options[index]);\n    };\n    const last = enabled[enabled.length - 1];\n\n    switch (event.key) {\n      case \"ArrowDown\":\n        return move(enabled.find((index) => index > position));\n      case \"ArrowUp\":\n        return move(findLast(enabled, (index) => index < position));\n      case \"Home\":\n        return move(enabled[0]);\n      case \"End\":\n        return move(last);\n      case \"PageDown\":\n        return move(enabled.find((index) => index >= position + 5) ?? last);\n      case \"PageUp\":\n        return move(findLast(enabled, (index) => index <= position - 5) ?? enabled[0]);\n      case \"ArrowLeft\":\n      case \"ArrowRight\": {\n        event.preventDefault();\n        const lists = Array.from(\n          context.getColumns()?.querySelectorAll<HTMLElement>(LIST_SELECTOR) ?? [],\n        );\n        const at = lists.indexOf(event.currentTarget);\n        lists[at + (event.key === \"ArrowRight\" ? 1 : -1)]?.focus();\n        return;\n      }\n      case \" \":\n        event.preventDefault();\n        if (highlighted && highlightIndex !== selectedIndex) pick(highlighted);\n        return;\n      case \"Enter\":\n        event.preventDefault();\n        if (highlighted && highlightIndex !== selectedIndex) pick(highlighted);\n        return context.confirm();\n      case \"Escape\":\n        /* Inline, Escape drops a draft; in the popover it closes it. */\n        if (!context.isOpen && context.hasDraft) {\n          event.preventDefault();\n          event.stopPropagation();\n          context.discardDraft();\n        }\n        return;\n    }\n\n    const key = normalizeTyped(event.key);\n    if (key.length !== 1) return;\n    if (type === \"period\") {\n      const found = enabled.filter((index) => {\n        const option = options[index];\n        return (\n          option.value === (key === \"a\" ? \"am\" : key === \"p\" ? \"pm\" : \"\") ||\n          normalizeTyped(option.label).startsWith(key)\n        );\n      });\n      if (found.length === 0) return;\n      /* The same letter again moves to the next match. */\n      move(found[(found.indexOf(highlightIndex) + 1) % found.length]);\n      return;\n    }\n    if (!/\\d/.test(key)) return;\n    /* Two digits within a second read as one number: 1 then 4 is 14. */\n    const buffer = typeahead.current;\n    const fresh = event.timeStamp - buffer.at > 1000;\n    let typed = !fresh && buffer.text.length === 1 ? buffer.text + key : key;\n    const find = (text: string) =>\n      options.findIndex((option) => !option.disabled && option.typed === Number(text));\n    let index = find(typed);\n    if (index === -1 && typed.length === 2) {\n      typed = key;\n      index = find(typed);\n    }\n    if (index === -1) {\n      /* \"3\" on a 15 minute step reaches 30. */\n      index = options.findIndex(\n        (option) =>\n          !option.disabled && option.typed !== null && pad(option.typed).startsWith(typed),\n      );\n    }\n    typeahead.current = { text: typed, at: event.timeStamp };\n    if (index !== -1) move(index);\n  };\n\n  const headerId = `${baseId}-label`;\n\n  return (\n    <div\n      data-slot=\"time-picker-column\"\n      data-type={type}\n      className={cn(COLUMN_CLASS, className)}\n      {...props}\n    >\n      <div id={headerId} data-slot=\"time-picker-column-label\" className={LABEL_CLASS}>\n        {label ?? i18n.labels[type]}\n      </div>\n      <div\n        ref={listRef}\n        role=\"listbox\"\n        aria-labelledby={headerId}\n        aria-describedby={`${context.id}-current`}\n        aria-activedescendant={highlightedId}\n        aria-disabled={context.isDisabled || undefined}\n        aria-readonly={context.isReadOnly || undefined}\n        tabIndex={interactive ? 0 : -1}\n        data-slot=\"time-picker-column-list\"\n        data-empty={highlighted ? undefined : \"\"}\n        data-fade={context.fade ? \"\" : undefined}\n        onKeyDown={interactive ? onKeyDown : undefined}\n        onScroll={(event) => syncOverflow(event.currentTarget)}\n        className={LIST_CLASS}\n      >\n        {options.map((option, index) => (\n          <div\n            key={option.value}\n            id={optionId(option)}\n            role=\"option\"\n            aria-selected={index === selectedIndex}\n            aria-disabled={option.disabled || undefined}\n            data-highlighted={index === highlightIndex ? \"\" : undefined}\n            data-slot=\"time-picker-option\"\n            onClick={editable ? () => pick(option) : undefined}\n            className={OPTION_CLASS}\n          >\n            {option.label}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction columnValueOf(time: TimePickerTime, type: TimePickerColumnType) {\n  return type === \"period\" ? periodOf(time.hour) : time[type];\n}\n\nfunction columnOptions(\n  type: TimePickerColumnType,\n  grid: TimeGrid,\n  flags: boolean[],\n  hourCycle: TimePickerHourCycle,\n  period: TimePickerPeriod,\n  i18n: TimePickerI18nConfig,\n): TimePickerOption[] {\n  const { formatSegment } = i18n.functions;\n  if (type === \"period\") {\n    return ([\"am\", \"pm\"] as const).map((value, index) => ({\n      value,\n      label: i18n.labels[value],\n      typed: null,\n      disabled: !flags[index],\n    }));\n  }\n  if (type === \"hour\") {\n    return grid.hours.flatMap((hour, index) => {\n      if (hourCycle === 12 && periodOf(hour) !== period) return [];\n      const shown = hourCycle === 12 ? hour % 12 || 12 : hour;\n      return [\n        {\n          value: hour,\n          label: formatSegment(shown, \"hour\"),\n          typed: shown,\n          disabled: !flags[index],\n        },\n      ];\n    });\n  }\n  const values = type === \"minute\" ? grid.minutes : grid.seconds;\n  return values.map((value, index) => ({\n    value,\n    label: formatSegment(value, type),\n    typed: value,\n    disabled: !flags[index],\n  }));\n}\n\n/**\n * The row the keyboard sits on: the selection; for a value off the steps\n * the enabled row below it; when empty, the first enabled time's row.\n */\nfunction highlightFor(\n  options: TimePickerOption[],\n  enabled: number[],\n  selectedIndex: number,\n  current: number | TimePickerPeriod | undefined,\n  first: number | TimePickerPeriod | undefined,\n) {\n  if (selectedIndex !== -1) return selectedIndex;\n  if (typeof current === \"number\") {\n    const below = findLast(enabled, (index) => {\n      const value = options[index].value;\n      return typeof value === \"number\" && value < current;\n    });\n    return below ?? enabled[0] ?? -1;\n  }\n  const preferred = enabled.find((index) => options[index].value === first);\n  return preferred ?? enabled[0] ?? -1;\n}\n\n/* `Array.prototype.findLast` is ES2023, newer than many consumers' `lib`. */\nfunction findLast<T>(items: T[], test: (item: T) => boolean) {\n  for (let index = items.length - 1; index >= 0; index--) {\n    if (test(items[index])) return items[index];\n  }\n  return undefined;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Footer                                   */\n/* -------------------------------------------------------------------------- */\n\ntype TimePickerFooterProps = React.ComponentProps<\"div\">;\n\n/** The action row. Without children: Now and Clear, plus OK in a popover or under `requireConfirm`. */\nfunction TimePickerFooter({ className, children, ...props }: TimePickerFooterProps) {\n  const context = useTimePickerContext(\"TimePickerFooter\");\n  const popup = React.useContext(SurfaceContext) === \"popup\";\n  return (\n    <div\n      data-slot=\"time-picker-footer\"\n      className={cn(\"flex items-center justify-between gap-2 border-t p-1.5\", className)}\n      {...props}\n    >\n      {children === undefined ? (\n        <>\n          <TimePickerNow />\n          <TimePickerClear />\n          {(popup || context.requireConfirm) && <TimePickerConfirm />}\n        </>\n      ) : (\n        children\n      )}\n    </div>\n  );\n}\n\ntype TimePickerActionProps = Omit<React.ComponentProps<typeof Button>, \"children\"> & {\n  children?: React.ReactNode;\n};\n\n/** Sets the current time, floored to the steps and moved inside the bounds. */\nfunction TimePickerNow({\n  variant = \"ghost\",\n  size = \"sm\",\n  onPress,\n  children,\n  ...props\n}: TimePickerActionProps) {\n  const context = useTimePickerContext(\"TimePickerNow\");\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      isDisabled={context.isDisabled || context.isReadOnly}\n      data-slot=\"time-picker-now\"\n      onPress={(event) => {\n        onPress?.(event);\n        pickNow(context);\n      }}\n      {...props}\n    >\n      {children === undefined ? context.i18n.labels.now : children}\n    </Button>\n  );\n}\n\n/** Empties the value, then hands focus to the first column. */\nfunction TimePickerClear({\n  variant = \"ghost\",\n  size = \"sm\",\n  className,\n  onPress,\n  children,\n  ...props\n}: TimePickerActionProps) {\n  const context = useTimePickerContext(\"TimePickerClear\");\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      isDisabled={context.isDisabled || context.isReadOnly}\n      data-slot=\"time-picker-clear\"\n      className={cn(\"text-muted-foreground\", className)}\n      onPress={(event) => {\n        onPress?.(event);\n        context.select(null);\n        context\n          .getColumns()\n          ?.querySelector<HTMLElement>(LIST_SELECTOR)\n          ?.focus({ preventScroll: true });\n      }}\n      {...props}\n    >\n      {children === undefined ? context.i18n.labels.clear : children}\n    </Button>\n  );\n}\n\n/** Commits a pending draft and closes the popover. */\nfunction TimePickerConfirm({\n  variant,\n  size = \"sm\",\n  onPress,\n  children,\n  ...props\n}: TimePickerActionProps) {\n  const context = useTimePickerContext(\"TimePickerConfirm\");\n  return (\n    <Button\n      variant={variant ?? (context.requireConfirm ? \"default\" : \"secondary\")}\n      size={size}\n      isDisabled={context.isDisabled}\n      data-slot=\"time-picker-confirm\"\n      onPress={(event) => {\n        onPress?.(event);\n        context.confirm();\n      }}\n      {...props}\n    >\n      {children === undefined ? context.i18n.labels.confirm : children}\n    </Button>\n  );\n}\n\nexport type {\n  TimePickerActionProps,\n  TimePickerApi,\n  TimePickerColumnProps,\n  TimePickerColumnsProps,\n  TimePickerColumnType,\n  TimePickerContentProps,\n  TimePickerFooterProps,\n  TimePickerFormatContext,\n  TimePickerFunctions,\n  TimePickerGranularity,\n  TimePickerHourCycle,\n  TimePickerI18nConfig,\n  TimePickerI18nOverrides,\n  TimePickerInputProps,\n  TimePickerLabels,\n  TimePickerPanelProps,\n  TimePickerPeriod,\n  TimePickerPeriodPosition,\n  TimePickerProps,\n  TimePickerTime,\n  TimePickerTriggerProps,\n  TimePickerUnit,\n  TimePickerValueProps,\n};\nexport {\n  DEFAULT_TIME_PICKER_I18N,\n  mergeTimePickerI18n,\n  parseTimeInput,\n  TimePicker,\n  TimePickerClear,\n  TimePickerColumn,\n  TimePickerColumns,\n  TimePickerConfirm,\n  TimePickerContent,\n  TimePickerFooter,\n  TimePickerInput,\n  TimePickerNow,\n  TimePickerPanel,\n  TimePickerTrigger,\n  TimePickerValue,\n  useTimePicker,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/time-picker.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "third-party",
    "license": "MIT",
    "origin": "reui.io (https://reui.io/docs/components/base/time-picker, MIT); rebuilt on react-aria Popover, Dialog and Button",
    "tags": [
      "forms",
      "time",
      "picker",
      "popover",
      "react-aria"
    ],
    "contentHash": "79c8a802fbb2"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}