{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-input",
  "title": "Time Input",
  "description": "A time field you type into, one segment at a time, that hands back a plain 24-hour clock string — \"09:30\", or \"09:30:15\" with seconds — and never a date and never a time zone. Reach for it wherever a form asks *when on the clock*: meeting, appointment and booking start and end times; opening hours and store hours; shift rosters, on-call rotations and availability editors; class and slot times; reminder, alarm and snooze times; quiet hours and do-not-disturb windows; the time half of a deadline or cut-off; the hour a scheduled report, digest email, backup or CI job runs; a maintenance window; delivery and pickup windows; check-in and check-out times; and the time part of a cron expression assembled in human terms. Common asks it answers: \"time input\", \"time picker\", \"time field\", \"hh:mm input\", \"24 hour time input\", \"12 hour time picker\", \"AM PM input\", \"time picker without a library\", \"keyboard time entry\", \"shadcn time picker\", \"shadcn time input\", \"input type=time replacement\", \"styled native time input\", \"opening hours input\", \"quiet hours picker\", \"start and end time picker\", \"meeting time input\", \"react-time-picker alternative\", \"MUI TimePicker equivalent\", \"antd TimePicker equivalent\", \"rc-time-picker alternative\". shadcn/ui ships nothing that touches the clock: fetching the source of all 63 items in its registry and grepping them turns up no type=\"time\", no hourCycle, no hour12 and no AM/PM anywhere. Its calendar is a react-day-picker wrapper that answers which day, input is a bare text box you would still have to parse, and input-otp is a fixed-length code with no time meaning. It is also the \"when\" half of a pair: duration-input answers *how long* — 90m, 1h30m, and 1:30 meaning a minute and a half of elapsed time — while this one answers what the clock reads, where 1:30 is half past one. Alongside date-input (the day), month-picker (the month) and timezone-select (which zone a time is meant in), this is the one that types the time. The work is in the parts that are easy to get wrong. \"Twelve-hour\" is really two different clocks and the component implements all four: en-US writes midnight 12 AM and counts 12, 1, 2 (h12) while ja-JP writes it 午前0時 and counts 0, 1, 2 (h11), and en-GB and de-DE are on 00–23 (h23) with h24 counting to 24 — the cycle comes from Intl rather than from a hardcoded guess, so nobody is shown an hour their locale does not write. The displayed 12 falls to hour 0 before the PM half is added, which is the off-by-twelve that quietly turns a noon deadline into a midnight one. Segment order, the separators and the AM/PM wording all come from the locale too — ko-KR puts the day period before the hour, ja-JP writes 午前/午後 — while the digits themselves are rendered as ASCII, so an ar-EG reader is not shown Arabic-Indic numerals that the number keys cannot reproduce. Auto-advance is decided by range rather than by counting to two: 5 jumps straight to the minute on a 24-hour clock because no hour starts with 5, 1 waits for a possible 10–19, and a lone 0 is already midnight there while on a twelve-hour clock it waits for the digit that makes it 01–09. A pair that cannot exist starts a new number instead of dropping the keystroke. Arrow keys step a segment and wrap, and the hour stays in its half of the day the way the native control does — 11 AM steps to 12 AM, not to noon. minuteStep rounds an off-step minute toward the arrow, so 07 on a 15-minute step gives 15 going up and 00 going down instead of 22 and 52. min and max are flagged with aria-invalid without ever blocking typing, and a max earlier than min is read as a range that wraps past midnight — the HTML rule for time inputs — which is what lets quiet hours of 22:00–06:00 or a night shift be one field. Every segment is a spinbutton with its own label and range, the day period is announced by name rather than as a bare number, and Backspace, Home, End and the left/right arrows move around the field. Pasting accepts \"14:30\", \"2:30 PM\" and the locale's own wording. No Date object is ever constructed and no zone is ever applied, so the value is a wall-clock time that survives being stored and read back anywhere. Works controlled or uncontrolled, forwards a ref to the first segment so a shortcut can focus it, and mirrors the value into a hidden input for native form submit. Theme-aware via shadcn tokens; no dependencies — no date library, no time picker package.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/ui/time-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype SegmentName = \"hour\" | \"minute\" | \"second\" | \"dayPeriod\"\n\ntype Part = { type: SegmentName } | { type: \"literal\"; value: string }\n\n// The four cycles CLDR actually uses, and they differ in more than twelve versus twenty-four:\n// midnight is \"0 AM\" in h11, \"12 AM\" in h12, \"00\" in h23 and \"24\" in h24. A field that only knows\n// h12 and h23 shows a ja-JP reader \"0 AM\" and turns a typed 24:00 into nothing.\ntype HourCycle = \"h11\" | \"h12\" | \"h23\" | \"h24\"\n\n// `hourCycle` on the resolved options is newer than the ES2020 lib a lot of projects still target,\n// so it is read through a local widening. Requiring a lib bump would make this component fail to\n// compile in a perfectly ordinary consumer project.\ntype ResolvedTimeOptions = Intl.ResolvedDateTimeFormatOptions & { hourCycle?: string }\n\ninterface TimeInputProps {\n  /** Controlled value as a 24-hour clock string — \"HH:mm\", or \"HH:mm:ss\" with `withSeconds`. \"\" while incomplete. */\n  value?: string\n  /** Initial value when uncontrolled, same shape as `value`. */\n  defaultValue?: string\n  /** Fires with the 24-hour string, or \"\" while any segment is still empty. */\n  onChange?: (value: string) => void\n  /** Add a seconds segment, and emit \"HH:mm:ss\". */\n  withSeconds?: boolean\n  /** Earliest valid time. Typing is never blocked; times outside the range are flagged with aria-invalid. */\n  min?: string\n  /** Latest valid time. When `max` is earlier than `min` the range wraps past midnight, so 22:00–06:00 means the night. */\n  max?: string\n  /** Arrow-key increment on the minute segment, in minutes. Off-step minutes round toward the arrow. */\n  minuteStep?: number\n  /** BCP-47 locale deciding segment order, the separators and the AM/PM wording. Defaults to the runtime's. */\n  locale?: string\n  /** Force a 12- or 24-hour clock. Left unset, the locale decides. */\n  hour12?: boolean\n  /** Disable every segment. */\n  disabled?: boolean\n  /** Accessible label for the field group (default \"Time\"). */\n  \"aria-label\"?: string\n  /** When set, a hidden input mirrors the 24-hour value so it submits with a native form. */\n  name?: string\n  className?: string\n}\n\ntype Fields = {\n  hour: number | null\n  minute: number | null\n  second: number | null\n  /** 0 = AM, 1 = PM. Kept alongside the hour so the period can be picked before the hour is typed. */\n  period: 0 | 1 | null\n}\n\nconst EMPTY_FIELDS: Fields = { hour: null, minute: null, second: null, period: null }\nconst EMPTY_BUF: Record<SegmentName, string> = { hour: \"\", minute: \"\", second: \"\", dayPeriod: \"\" }\n\nconst LABELS: Record<SegmentName, string> = {\n  hour: \"Hour\",\n  minute: \"Minute\",\n  second: \"Second\",\n  dayPeriod: \"AM/PM\",\n}\nconst PLACEHOLDERS: Record<SegmentName, string> = {\n  hour: \"hh\",\n  minute: \"mm\",\n  second: \"ss\",\n  dayPeriod: \"--\",\n}\n\nconst HOUR_RANGE: Record<HourCycle, { min: number; max: number }> = {\n  h11: { min: 0, max: 11 },\n  h12: { min: 1, max: 12 },\n  h23: { min: 0, max: 23 },\n  h24: { min: 1, max: 24 },\n}\n\nconst is12Hour = (cycle: HourCycle) => cycle === \"h11\" || cycle === \"h12\"\n\n// Ask the runtime which clock this locale writes rather than assuming one: en-US is on twelve hours\n// and en-GB, de-DE and ja-JP are on twenty-four, so a hardcoded choice is wrong for most readers.\n// An explicit `hour12` still goes through Intl, because \"twelve hours\" means h11 in ja-JP and h12 in\n// en-US and only the locale knows which.\nfunction resolveCycle(locale: string | undefined, hour12: boolean | undefined): HourCycle {\n  try {\n    const opts = hour12 === undefined ? { hour: \"numeric\" as const } : { hour: \"numeric\" as const, hour12 }\n    const resolved = new Intl.DateTimeFormat(locale, opts).resolvedOptions() as ResolvedTimeOptions\n    const cycle = resolved.hourCycle\n    if (cycle === \"h11\" || cycle === \"h12\" || cycle === \"h23\" || cycle === \"h24\") return cycle\n    // Older runtimes report hour12 without hourCycle.\n    if (typeof resolved.hour12 === \"boolean\") return resolved.hour12 ? \"h12\" : \"h23\"\n  } catch {\n    // Intl missing, or the locale tag is malformed.\n  }\n  return hour12 ? \"h12\" : \"h23\"\n}\n\nfunction fallbackParts(cycle: HourCycle, withSeconds: boolean): Part[] {\n  const out: Part[] = [{ type: \"hour\" }, { type: \"literal\", value: \":\" }, { type: \"minute\" }]\n  if (withSeconds) out.push({ type: \"literal\", value: \":\" }, { type: \"second\" })\n  if (is12Hour(cycle)) out.push({ type: \"literal\", value: \" \" }, { type: \"dayPeriod\" })\n  return out\n}\n\n// Segment order and separators come from Intl too. ko-KR puts the day period *before* the hour and\n// ja-JP writes it with no space after it, neither of which a hand-built layout gets right.\nfunction buildParts(locale: string | undefined, cycle: HourCycle, withSeconds: boolean): Part[] {\n  try {\n    const fmt = new Intl.DateTimeFormat(locale, {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      ...(withSeconds ? { second: \"2-digit\" as const } : null),\n      hourCycle: cycle,\n      timeZone: \"UTC\",\n    })\n    // A runtime too old to know `hourCycle` ignores it silently and lays the segments out for the\n    // other clock, so the resolved value is checked instead of trusted.\n    if ((fmt.resolvedOptions() as ResolvedTimeOptions).hourCycle !== cycle) {\n      return fallbackParts(cycle, withSeconds)\n    }\n    const out: Part[] = []\n    for (const p of fmt.formatToParts(new Date(Date.UTC(2026, 0, 2, 13, 5, 7)))) {\n      if (p.type === \"hour\" || p.type === \"minute\" || p.type === \"second\" || p.type === \"dayPeriod\") {\n        out.push({ type: p.type })\n      } else if (p.type === \"literal\") {\n        out.push({ type: \"literal\", value: p.value })\n      }\n    }\n    const named = new Set(out.filter((p) => p.type !== \"literal\").map((p) => p.type))\n    const wanted = 2 + (withSeconds ? 1 : 0) + (is12Hour(cycle) ? 1 : 0)\n    if (named.size === wanted) return out\n  } catch {\n    // Intl missing, or the locale tag is malformed.\n  }\n  return fallbackParts(cycle, withSeconds)\n}\n\n// The AM and PM wording as this locale writes it — \"AM\", \"午前\", \"오전\", \"ص\". Only the two names are\n// taken from Intl; the digits are rendered here, because ar-EG would otherwise hand back\n// Arabic-Indic numerals that cannot be typed back into the field.\nfunction dayPeriodNames(locale: string | undefined, cycle: HourCycle): [string, string] {\n  const read = (hour: number, fallback: string) => {\n    try {\n      const parts = new Intl.DateTimeFormat(locale, {\n        hour: \"numeric\",\n        hourCycle: cycle,\n        timeZone: \"UTC\",\n      }).formatToParts(new Date(Date.UTC(2026, 0, 2, hour)))\n      return parts.find((p) => p.type === \"dayPeriod\")?.value ?? fallback\n    } catch {\n      return fallback\n    }\n  }\n  return [read(9, \"AM\"), read(21, \"PM\")]\n}\n\nfunction toDisplayHour(hour: number, cycle: HourCycle) {\n  if (cycle === \"h23\") return hour\n  if (cycle === \"h24\") return hour === 0 ? 24 : hour\n  const wrapped = hour % 12\n  return cycle === \"h12\" ? (wrapped === 0 ? 12 : wrapped) : wrapped\n}\n\n// The off-by-twelve that hand-rolled 12-hour fields are famous for: 12 AM is midnight and 12 PM is\n// noon, so the displayed 12 has to fall to 0 before the PM half is added.\nfunction fromDisplayHour(display: number, period: 0 | 1 | null, cycle: HourCycle) {\n  if (cycle === \"h23\") return display\n  if (cycle === \"h24\") return display === 24 ? 0 : display\n  const base = cycle === \"h12\" ? display % 12 : display\n  return base + (period === 1 ? 12 : 0)\n}\n\n// The hour is deliberately not settable here. It has to travel through `withHour` so the day period\n// moves with it, and leaving it out of this signature makes that a compile error rather than a\n// convention someone has to remember.\nfunction setField(f: Fields, seg: Exclude<SegmentName, \"hour\">, value: number | null): Fields {\n  if (seg === \"minute\") return { ...f, minute: value }\n  if (seg === \"second\") return { ...f, second: value }\n  return withPeriod(f, value === null ? null : value === 1 ? 1 : 0)\n}\n\n// Setting the period moves the hour with it, and setting the hour re-derives the period, so the two\n// can never disagree about whether 13:00 is showing PM.\nfunction withPeriod(f: Fields, period: 0 | 1 | null): Fields {\n  if (f.hour === null || period === null) return { ...f, period }\n  const hour = (f.hour % 12) + (period === 1 ? 12 : 0)\n  return { ...f, hour, period }\n}\n\nfunction withHour(f: Fields, hour: number | null): Fields {\n  if (hour === null) return { ...f, hour: null }\n  return { ...f, hour, period: hour >= 12 ? 1 : 0 }\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\")\n\nfunction toValue(f: Fields, withSeconds: boolean) {\n  const { hour, minute, second } = f\n  if (hour === null || minute === null) return \"\"\n  const hm = `${pad(hour)}:${pad(minute)}`\n  if (!withSeconds) return hm\n  return second === null ? \"\" : `${hm}:${pad(second)}`\n}\n\n// Accepts \"HH:mm\" and \"HH:mm:ss\" — the same shape a native time input produces. A full timestamp is\n// rejected rather than guessed at, because reading one applies a time zone and can shift the clock.\nfunction parseValue(text: string | undefined): Fields {\n  const m = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec((text ?? \"\").trim())\n  if (!m) return EMPTY_FIELDS\n  const hour = Number(m[1])\n  const minute = Number(m[2])\n  const second = m[3] === undefined ? null : Number(m[3])\n  if (hour > 23 || minute > 59 || (second !== null && second > 59)) return EMPTY_FIELDS\n  return { hour, minute, second, period: hour >= 12 ? 1 : 0 }\n}\n\n// One typed digit. `advance` reports that no further digit could extend this segment, which is what\n// makes \"5\" jump straight to the minute on a 24-hour clock while \"1\" waits for a possible 10-19.\n// `min` is what lets midnight be typed: on h23 a lone \"0\" is already hour zero, while on h12 it has\n// to wait for the digit that turns it into 01-09. A pair that would be out of range starts a new\n// number instead of being dropped, so no keystroke is ever lost.\nfunction typeDigit(buf: string, digit: string, min: number, max: number) {\n  const combined = Number(buf + digit)\n  if (buf !== \"\" && combined >= min && combined <= max) {\n    return { buf: \"\", value: combined, advance: true }\n  }\n  const d = Number(digit)\n  if (d < min) return { buf: digit, value: null, advance: false }\n  if (d * 10 > max) return { buf: \"\", value: d, advance: true }\n  return { buf: digit, value: d, advance: false }\n}\n\nfunction wrap(value: number, min: number, max: number) {\n  const span = max - min + 1\n  return ((((value - min) % span) + span) % span) + min\n}\n\n// Arrows wrap every segment, because unlike a year there is nothing past the end of a clock. The\n// hour steps inside the displayed twelve and leaves AM/PM alone, which is what the native control\n// does: 11 → 12 → 1 without silently jumping the reader half a day.\nfunction stepSegment(\n  f: Fields,\n  seg: SegmentName,\n  delta: number,\n  cycle: HourCycle,\n  minuteStep: number,\n  now: Fields\n): Fields {\n  if (seg === \"dayPeriod\") {\n    const cur = f.period ?? now.period ?? 0\n    return withPeriod(f, f.period === null ? cur : cur === 1 ? 0 : 1)\n  }\n  const cur = f[seg]\n  // The first press on an empty segment seeds from the current time rather than jumping to zero,\n  // which is what makes the arrows usable for a time near now.\n  if (cur === null) {\n    if (seg === \"hour\") return withHour(f, now.hour)\n    return setField(f, seg, now[seg])\n  }\n  if (seg === \"hour\") {\n    const range = HOUR_RANGE[cycle]\n    const next = wrap(toDisplayHour(cur, cycle) + delta, range.min, range.max)\n    return { ...f, hour: fromDisplayHour(next, f.period, cycle) }\n  }\n  if (seg === \"second\") return setField(f, seg, wrap(cur + delta, 0, 59))\n  // An off-step minute rounds toward the arrow first, so 07 with a 15-minute step gives 15 going up\n  // and 00 going down instead of 22 and 52.\n  const step = Math.max(1, Math.floor(minuteStep))\n  const rounded = delta > 0 ? Math.ceil(cur / step) * step : Math.floor(cur / step) * step\n  const next = rounded === cur ? cur + delta * step : rounded\n  return setField(f, seg, wrap(next, 0, 59))\n}\n\n// Bounds are padded to a common width so a \"09:30\" value can be compared with a \"09:30:00\" bound.\nfunction normalizeBound(text: string | undefined) {\n  const m = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec((text ?? \"\").trim())\n  if (!m) return null\n  if (Number(m[1]) > 23 || Number(m[2]) > 59 || (m[3] !== undefined && Number(m[3]) > 59)) return null\n  return `${m[1]}:${m[2]}:${m[3] ?? \"00\"}`\n}\n\n// Zero-padded clock strings sort chronologically, so no Date (and no time zone) has to be involved.\n// A max earlier than min is a range that wraps past midnight — the HTML rule for time inputs, and\n// the only way to express quiet hours or a night shift as a single field.\nfunction outOfRange(value: string, min: string | undefined, max: string | undefined) {\n  if (value === \"\") return false\n  const v = normalizeBound(value)\n  if (v === null) return false\n  const lo = normalizeBound(min)\n  const hi = normalizeBound(max)\n  if (lo !== null && hi !== null && lo > hi) return v < lo && v > hi\n  if (lo !== null && v < lo) return true\n  if (hi !== null && v > hi) return true\n  return false\n}\n\n// A pasted time arrives either as a 24-hour string or written the way the field displays it, so the\n// day period is read from the text — in this locale's wording as well as in English — rather than\n// assumed. Returns null when the text is not a time, so the paste is ignored rather than half\n// applied.\nfunction parsePasted(\n  text: string,\n  withSeconds: boolean,\n  names: [string, string]\n): Fields | null {\n  const trimmed = text.trim()\n  const m = /(\\d{1,2})\\s*:\\s*(\\d{1,2})(?:\\s*:\\s*(\\d{1,2}))?/.exec(trimmed)\n  if (!m) return null\n  const lower = trimmed.toLowerCase()\n  const before = lower.slice(0, m.index)\n  const after = lower.slice(m.index + m[0].length)\n  const near = `${before} ${after}`\n  const has = (name: string) => name.trim() !== \"\" && near.includes(name.toLowerCase())\n  const pm = has(names[1]) || /\\bp\\.?m\\.?/.test(near)\n  const am = has(names[0]) || /\\ba\\.?m\\.?/.test(near)\n\n  let hour = Number(m[1])\n  const minute = Number(m[2])\n  const second = m[3] === undefined ? null : Number(m[3])\n  if (minute > 59 || (second !== null && second > 59)) return null\n  if (am || pm) {\n    // Always read with h12 meaning, even on an h11 locale: a person writes \"12:30 PM\" for half past\n    // noon whichever clock their locale renders, and h11 arithmetic would turn that into hour 24.\n    if (hour > 12) return null\n    hour = fromDisplayHour(hour, pm ? 1 : 0, \"h12\")\n  } else if (hour === 24 && minute === 0 && (second ?? 0) === 0) {\n    hour = 0\n  }\n  if (hour > 23) return null\n  return {\n    hour,\n    minute,\n    // A time written without seconds is a time at the top of the minute, not an unreadable one.\n    second: withSeconds ? (second ?? 0) : second,\n    period: hour >= 12 ? 1 : 0,\n  }\n}\n\nexport const TimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(function TimeInput(\n  {\n    value,\n    defaultValue,\n    onChange,\n    withSeconds = false,\n    min,\n    max,\n    minuteStep = 1,\n    locale,\n    hour12,\n    disabled,\n    name,\n    className,\n    \"aria-label\": ariaLabel = \"Time\",\n  },\n  forwardedRef\n) {\n  const isControlled = value !== undefined\n  const [fields, setFields] = React.useState<Fields>(() =>\n    parseValue(isControlled ? value : defaultValue)\n  )\n  const [buf, setBuf] = React.useState<Record<SegmentName, string>>(EMPTY_BUF)\n\n  const cycle = React.useMemo(() => resolveCycle(locale, hour12), [locale, hour12])\n  const parts = React.useMemo(\n    () => buildParts(locale, cycle, withSeconds),\n    [locale, cycle, withSeconds]\n  )\n  const periodNames = React.useMemo(() => dayPeriodNames(locale, cycle), [locale, cycle])\n  const order = React.useMemo(\n    () => parts.filter((p): p is { type: SegmentName } => p.type !== \"literal\").map((p) => p.type),\n    [parts]\n  )\n\n  const segRefs = React.useRef<Record<SegmentName, HTMLInputElement | null>>({\n    hour: null,\n    minute: null,\n    second: null,\n    dayPeriod: null,\n  })\n\n  // Forward the first segment in reading order so callers can focus the field from a shortcut.\n  React.useImperativeHandle(forwardedRef, () => segRefs.current[order[0]] as HTMLInputElement, [order])\n\n  const fieldsRef = React.useRef(fields)\n  fieldsRef.current = fields\n\n  // Re-seed from the prop only when it disagrees with what is on screen. A controlled parent stores\n  // \"\" for an incomplete time, and echoing that back blindly would erase the half-typed segments on\n  // every keystroke.\n  React.useEffect(() => {\n    if (!isControlled) return\n    if ((value ?? \"\") !== toValue(fieldsRef.current, withSeconds)) {\n      setFields(parseValue(value))\n      setBuf(EMPTY_BUF)\n    }\n  }, [isControlled, value, withSeconds])\n\n  const current = toValue(fields, withSeconds)\n  const invalid = outOfRange(current, min, max)\n\n  function rangeFor(seg: SegmentName) {\n    if (seg === \"hour\") return HOUR_RANGE[cycle]\n    if (seg === \"dayPeriod\") return { min: 0, max: 1 }\n    return { min: 0, max: 59 }\n  }\n\n  function displayValue(seg: SegmentName) {\n    if (seg === \"dayPeriod\") return fields.period\n    if (seg === \"hour\") return fields.hour === null ? null : toDisplayHour(fields.hour, cycle)\n    return fields[seg]\n  }\n\n  function setBufFor(seg: SegmentName, next: string) {\n    setBuf((b) => ({ ...b, [seg]: next }))\n  }\n\n  function focusIndex(i: number) {\n    const seg = order[Math.max(0, Math.min(i, order.length - 1))]\n    const el = segRefs.current[seg]\n    el?.focus()\n    el?.select()\n  }\n\n  function focusRelative(seg: SegmentName, delta: number) {\n    focusIndex(order.indexOf(seg) + delta)\n  }\n\n  // Single commit path. The segments stay local state even when the value is controlled, because a\n  // half-typed time has no value to flow back through the prop: a parent holding \"\" for an\n  // incomplete time would drop every keystroke but the last, and the field would never fill in. The\n  // effect above is what keeps the parent authoritative — as soon as the segments read as a complete\n  // time that disagrees with the prop, they are pulled back to it.\n  function commit(next: Fields) {\n    setFields(next)\n    onChange?.(toValue(next, withSeconds))\n  }\n\n  function input(seg: SegmentName, digit: string) {\n    const range = rangeFor(seg)\n    const r = typeDigit(buf[seg], digit, range.min, range.max)\n    setBufFor(seg, r.buf)\n    if (seg === \"hour\") {\n      commit(withHour(fields, r.value === null ? null : fromDisplayHour(r.value, fields.period, cycle)))\n    } else {\n      commit(setField(fields, seg, r.value))\n    }\n    if (r.advance) focusRelative(seg, 1)\n  }\n\n  // The day period answers to the first letter of either the English or the localised name, so an\n  // en-US reader types A or P and a ja-JP reader can still drive it from the arrows.\n  function typePeriod(key: string) {\n    const k = key.toLowerCase()\n    const first = (s: string) => s.trim().toLowerCase().slice(0, 1)\n    const want = (name: string, ascii: string) =>\n      k === ascii || (first(name) !== \"\" && k === first(name))\n    const period = want(periodNames[0], \"a\") ? 0 : want(periodNames[1], \"p\") ? 1 : null\n    if (period === null) return false\n    // Typing the period it is already on is a keystroke, not a change: emitting here would churn a\n    // controlled parent on every repeat press.\n    if (fields.period !== period) commit(withPeriod(fields, period))\n    return true\n  }\n\n  function handleKeyDown(seg: SegmentName, e: React.KeyboardEvent<HTMLInputElement>) {\n    if (disabled) return\n    switch (e.key) {\n      case \"ArrowUp\":\n      case \"ArrowDown\": {\n        e.preventDefault()\n        // `new Date()` is read here in the handler and never during render: seeding \"now\" while\n        // rendering would disagree between the server and the client and break hydration.\n        const d = new Date()\n        const now: Fields = {\n          hour: d.getHours(),\n          minute: d.getMinutes(),\n          second: d.getSeconds(),\n          period: d.getHours() >= 12 ? 1 : 0,\n        }\n        setBufFor(seg, \"\")\n        commit(stepSegment(fields, seg, e.key === \"ArrowUp\" ? 1 : -1, cycle, minuteStep, now))\n        break\n      }\n      case \"ArrowLeft\":\n        e.preventDefault()\n        focusRelative(seg, -1)\n        break\n      case \"ArrowRight\":\n        e.preventDefault()\n        focusRelative(seg, 1)\n        break\n      case \"Home\":\n        e.preventDefault()\n        focusIndex(0)\n        break\n      case \"End\":\n        e.preventDefault()\n        focusIndex(order.length - 1)\n        break\n      case \"Backspace\":\n      case \"Delete\": {\n        e.preventDefault()\n        const filled = seg === \"hour\" ? fields.hour !== null : displayValue(seg) !== null\n        if (buf[seg] !== \"\" || filled) {\n          setBufFor(seg, \"\")\n          commit(seg === \"hour\" ? withHour(fields, null) : setField(fields, seg, null))\n        } else if (e.key === \"Backspace\") {\n          focusRelative(seg, -1)\n        }\n        break\n      }\n      default: {\n        if (e.metaKey || e.ctrlKey || e.altKey) return\n        if (seg === \"dayPeriod\") {\n          if (typePeriod(e.key)) e.preventDefault()\n          return\n        }\n        if (/^\\d$/.test(e.key)) {\n          e.preventDefault()\n          input(seg, e.key)\n        }\n      }\n    }\n  }\n\n  // Virtual keyboards often report key=\"Unidentified\" on keydown, so the typed character is read\n  // from the input event instead. Desktop never reaches here because keydown already consumed it.\n  // Only the first digit is taken: `input` reads the state of this render, so applying several in a\n  // row would have each one overwrite the last. Multi-character text is a paste, and onPaste handles\n  // it in one commit.\n  function handleBeforeInput(seg: SegmentName, e: React.FormEvent<HTMLInputElement>) {\n    if (disabled) return\n    e.preventDefault()\n    const data = (e.nativeEvent as InputEvent).data\n    if (!data) return\n    if (seg === \"dayPeriod\") {\n      for (const ch of Array.from(data)) if (typePeriod(ch)) return\n      return\n    }\n    const digit = Array.from(data).find((ch) => ch >= \"0\" && ch <= \"9\")\n    if (digit) input(seg, digit)\n  }\n\n  function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {\n    e.preventDefault()\n    if (disabled) return\n    const parsed = parsePasted(e.clipboardData.getData(\"text\"), withSeconds, periodNames)\n    if (!parsed) return\n    setBuf(EMPTY_BUF)\n    commit(parsed)\n    focusIndex(order.length - 1)\n  }\n\n  function display(seg: SegmentName) {\n    if (buf[seg] !== \"\") return buf[seg]\n    const v = displayValue(seg)\n    if (v === null) return PLACEHOLDERS[seg]\n    return seg === \"dayPeriod\" ? periodNames[v] : pad(v)\n  }\n\n  function valueText(seg: SegmentName) {\n    const v = displayValue(seg)\n    if (v === null) return \"Empty\"\n    // A bare spinbutton would have the reader hear the period as \"1\"; the name is what identifies it.\n    return seg === \"dayPeriod\" ? periodNames[v] : String(v)\n  }\n\n  // The period is as wide as the longer of the two names so the field does not resize when it flips.\n  const periodWidth = Math.max(2, periodNames[0].trim().length, periodNames[1].trim().length)\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={ariaLabel}\n      className={cn(\n        \"inline-flex h-9 items-center rounded-md border border-input bg-transparent px-3 py-1 font-mono text-sm shadow-sm transition-colors\",\n        \"focus-within:outline-none focus-within:ring-1 focus-within:ring-ring\",\n        invalid && \"border-destructive focus-within:ring-destructive\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className\n      )}\n    >\n      {parts.map((p, i) =>\n        p.type === \"literal\" ? (\n          <span key={i} aria-hidden=\"true\" className=\"text-muted-foreground\">\n            {p.value}\n          </span>\n        ) : (\n          <input\n            key={i}\n            ref={(el) => {\n              segRefs.current[p.type] = el\n            }}\n            type=\"text\"\n            inputMode={p.type === \"dayPeriod\" ? \"text\" : \"numeric\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n            role=\"spinbutton\"\n            disabled={disabled}\n            aria-label={LABELS[p.type]}\n            aria-valuenow={displayValue(p.type) ?? undefined}\n            aria-valuemin={rangeFor(p.type).min}\n            aria-valuemax={rangeFor(p.type).max}\n            aria-valuetext={valueText(p.type)}\n            aria-invalid={invalid || undefined}\n            value={display(p.type)}\n            // Every mutation goes through onKeyDown/onBeforeInput. This keeps React from warning\n            // about a controlled input with no change handler, and anything an exotic IME slips past\n            // both is discarded by the next render.\n            onChange={() => {}}\n            onKeyDown={(e) => handleKeyDown(p.type, e)}\n            onBeforeInput={(e) => handleBeforeInput(p.type, e)}\n            onPaste={handlePaste}\n            onFocus={(e) => e.currentTarget.select()}\n            onBlur={() => setBufFor(p.type, \"\")}\n            className={cn(\n              \"rounded-sm bg-transparent text-center tabular-nums caret-transparent outline-none\",\n              \"focus:bg-accent focus:text-accent-foreground\",\n              \"disabled:cursor-not-allowed\",\n              displayValue(p.type) === null && \"text-muted-foreground\"\n            )}\n            style={{ width: p.type === \"dayPeriod\" ? `${periodWidth}ch` : \"2ch\" }}\n          />\n        )\n      )}\n      {name ? <input type=\"hidden\" name={name} value={current} /> : null}\n    </div>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}