{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "type": "registry:ui",
  "title": "Time Picker",
  "description": "Segmented time field in the locale's order, with rolling digits and a slot list.",
  "categories": [
    "dates"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/time-picker.tsx",
      "content": "\"use client\";\nimport { Select } from \"@base-ui/react/select\";\nimport NumberFlow from \"@number-flow/react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\ntype Seg = \"hour\" | \"minute\" | \"period\";\ntype Cycle = 12 | 24;\n/** A partly typed time. `h` is in the display cycle (1–12 or 0–23); `p` is 0 for AM, 1 for PM. */\ntype Draft = { h: number | null; m: number | null; p: 0 | 1 | null };\ntype Piece = { kind: \"seg\"; seg: Seg } | { kind: \"lit\"; text: string };\ntype Roll = { source: \"step\" | \"pick\" | \"type\"; seg?: Seg; trend?: 1 | -1 };\n\n/* ------------------------------------------------------------------------ */\n/* Time maths. Values are \"HH:mm\" in 24-hour time, like <input type=\"time\">. */\n\nconst parse = (v: string | null | undefined) => {\n  const m = v ? /^(\\d{1,2}):(\\d{2})/.exec(v) : null;\n  if (!m) return null;\n  const h = Number(m[1]);\n  const min = Number(m[2]);\n  return h < 24 && min < 60 ? h * 60 + min : null;\n};\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst toValue = (mins: number) => `${pad(Math.floor(mins / 60))}:${pad(mins % 60)}`;\n\nfunction draftFrom(value: string | null, cycle: Cycle): Draft {\n  const mins = parse(value);\n  if (mins == null) return { h: null, m: null, p: null };\n  const h24 = Math.floor(mins / 60);\n  return cycle === 24 ? { h: h24, m: mins % 60, p: null } : { h: h24 % 12 || 12, m: mins % 60, p: h24 >= 12 ? 1 : 0 };\n}\n\nfunction valueFrom(d: Draft, cycle: Cycle): string | null {\n  if (d.h == null || d.m == null) return null;\n  if (cycle === 24) return toValue(d.h * 60 + d.m);\n  if (d.p == null) return null;\n  return toValue(((d.h % 12) + d.p * 12) * 60 + d.m);\n}\n\nconst range = (seg: Seg, cycle: Cycle): [number, number] => (seg === \"minute\" ? [0, 59] : seg === \"period\" ? [0, 1] : cycle === 12 ? [1, 12] : [0, 23]);\n\nconst wrap = (n: number, [lo, hi]: [number, number]) => {\n  const size = hi - lo + 1;\n  return ((((n - lo) % size) + size) % size) + lo;\n};\n\n/* ------------------------------------------------------------------------ */\n/* Locale. The server can't know the reader's locale, so it renders en-US and */\n/* the client switches on hydration without a mismatch.                       */\n\nconst noop = () => () => {};\nfunction useLocale(locale?: string) {\n  const detected = useSyncExternalStore(\n    noop,\n    () => Intl.DateTimeFormat().resolvedOptions().locale,\n    () => \"en-US\",\n  );\n  return locale ?? detected;\n}\n\nfunction describeLocale(locale: string, forced?: Cycle) {\n  const auto = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions().hourCycle;\n  const cycle: Cycle = forced ?? (auto === \"h11\" || auto === \"h12\" ? 12 : 24);\n  const format = new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", hourCycle: cycle === 12 ? \"h12\" : \"h23\" });\n  const at = (h: number, m = 45) => format.formatToParts(new Date(2000, 0, 1, h, m));\n\n  // Segment order and separators come from the locale: \"1:45 PM\", \"13:45\", \"13.45\", \"오후 1:45\".\n  let pieces: Piece[] = [];\n  for (const part of at(13)) {\n    if (part.type === \"hour\" || part.type === \"minute\") pieces.push({ kind: \"seg\", seg: part.type });\n    else if (part.type === \"dayPeriod\" && cycle === 12) pieces.push({ kind: \"seg\", seg: \"period\" });\n    else if (part.type === \"literal\") pieces.push({ kind: \"lit\", text: part.value.replace(/\\s+/g, \" \") });\n  }\n  while (pieces[0]?.kind === \"lit\") pieces.shift();\n  while (pieces.at(-1)?.kind === \"lit\") pieces.pop();\n  const segs = pieces.flatMap((p) => (p.kind === \"seg\" ? [p.seg] : []));\n  if (!segs.includes(\"hour\") || !segs.includes(\"minute\") || (cycle === 12 && !segs.includes(\"period\"))) {\n    pieces = [\n      { kind: \"seg\", seg: \"hour\" },\n      { kind: \"lit\", text: \":\" },\n      { kind: \"seg\", seg: \"minute\" },\n    ];\n    if (cycle === 12) pieces.push({ kind: \"lit\", text: \" \" }, { kind: \"seg\", seg: \"period\" });\n  }\n\n  const periodOf = (h: number) => at(h).find((p) => p.type === \"dayPeriod\")?.value ?? (h < 12 ? \"AM\" : \"PM\");\n  const periods: [string, string] = [periodOf(9), periodOf(21)];\n  // 24-hour time always reads as 08:00; 12-hour follows the locale (\"9:05 AM\" in en-US).\n  const padHour = cycle === 24 || (at(9, 5).find((p) => p.type === \"hour\")?.value.length ?? 1) > 1;\n\n  let fieldNames: Record<Seg, string> = { hour: \"Hour\", minute: \"Minute\", period: \"AM/PM\" };\n  try {\n    const names = new Intl.DisplayNames(locale, { type: \"dateTimeField\" });\n    fieldNames = { hour: names.of(\"hour\") ?? \"Hour\", minute: names.of(\"minute\") ?? \"Minute\", period: names.of(\"dayPeriod\") ?? \"AM/PM\" };\n  } catch {}\n\n  return { cycle, format, pieces, periods, padHour, fieldNames };\n}\n\n/** \"45 min\", \"1 hr\", \"1 hr 15 min\", with each unit in the locale's words. */\nfunction durationLabel(locale: string, mins: number) {\n  const unit = (unit: \"hour\" | \"minute\", n: number) => new Intl.NumberFormat(locale, { style: \"unit\", unit, unitDisplay: \"short\" }).format(n);\n  const h = Math.floor(mins / 60);\n  const m = mins % 60;\n  return [h && unit(\"hour\", h), (m || !h) && unit(\"minute\", m)].filter(Boolean).join(\" \");\n}\n\nconst expo = `cubic-bezier(${ease.out.join(\",\")})`;\nconst rollTiming = { duration: 240, easing: expo };\n\n/* ------------------------------------------------------------------------ */\n\nexport type TimePickerProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\" | \"children\"> & {\n  /** \"HH:mm\" in 24-hour time, or null when empty. */\n  value?: string | null;\n  defaultValue?: string | null;\n  /** Called with a complete \"HH:mm\", or null when the field is cleared or only partly filled. */\n  onValueChange?: (value: string | null) => void;\n  /** Minutes between the suggested times in the list. */\n  interval?: number;\n  /** Minutes the arrow keys move the minute segment, snapping to that grid. */\n  step?: number;\n  /** Earliest allowed time, \"HH:mm\". The list starts here and earlier typed times are invalid. */\n  min?: string;\n  /** Latest allowed time, \"HH:mm\". */\n  max?: string;\n  /** Start time for an end-time field: the list starts after it and each option shows its duration. */\n  durationFrom?: string | null;\n  /** Grays out options in the list, for example times that are already booked. */\n  isTimeDisabled?: (value: string) => boolean;\n  /** Force 12 or 24-hour time. By default it follows the locale. */\n  hourCycle?: Cycle;\n  locale?: string;\n  size?: \"sm\" | \"md\";\n  disabled?: boolean;\n  readOnly?: boolean;\n  /** Shows the error treatment. Times outside min and max are always invalid. */\n  invalid?: boolean;\n  /** Submits the value with a form, as \"HH:mm\". */\n  name?: string;\n  /** Accessible name of the list button. */\n  listLabel?: string;\n};\n\nexport function TimePicker({\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  interval = 30,\n  step = 1,\n  min,\n  max,\n  durationFrom,\n  isTimeDisabled,\n  hourCycle,\n  locale: localeProp,\n  size = \"md\",\n  disabled = false,\n  readOnly = false,\n  invalid: invalidProp = false,\n  name,\n  listLabel = \"Show suggested times\",\n  className,\n  onPointerDown,\n  ...rest\n}: TimePickerProps) {\n  const locale = useLocale(localeProp);\n  const info = useMemo(() => describeLocale(locale, hourCycle), [locale, hourCycle]);\n  const { cycle } = info;\n  const reduce = useReducedMotion();\n\n  const [value, setValue] = useControllableState<string | null>({ value: valueProp, defaultValue, onChange: onValueChange });\n  const [draft, setDraft] = useState<Draft>(() => draftFrom(value, cycle));\n  const [synced, setSynced] = useState({ value, cycle });\n  const [roll, setRoll] = useState<Roll>({ source: \"type\" });\n  const [typed, setTyped] = useState<{ seg: Seg; text: string } | null>(null);\n  const [open, setOpen] = useState(false);\n  const fieldRef = useRef<HTMLDivElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n  const segRefs = useRef<Partial<Record<Seg, HTMLDivElement | null>>>({});\n\n  // Follow the value when it changes from outside (a controlled parent, the list,\n  // a locale switch), without wiping a time that is still being typed.\n  if (synced.value !== value || synced.cycle !== cycle) {\n    setSynced({ value, cycle });\n    if (synced.cycle !== cycle || valueFrom(draft, cycle) !== value) {\n      setDraft(draftFrom(value, cycle));\n      setRoll({ source: \"pick\" });\n    }\n  }\n\n  const order = info.pieces.flatMap((p) => (p.kind === \"seg\" ? [p.seg] : []));\n  const minM = parse(min);\n  const maxM = parse(max);\n  const fromM = parse(durationFrom);\n  const current = parse(value);\n  const outOfRange = current != null && ((minM != null && current < minM) || (maxM != null && current > maxM));\n  const invalid = invalidProp || outOfRange;\n\n  const slots = useMemo(() => {\n    const first = fromM != null ? fromM + interval : (minM ?? 0);\n    const last = Math.min(maxM ?? 1439, 1439);\n    const out: { value: string; label: string; duration?: string; disabled: boolean }[] = [];\n    for (let t = first; t <= last && interval > 0; t += interval) {\n      const v = toValue(t);\n      out.push({\n        value: v,\n        label: info.format.format(new Date(2000, 0, 1, Math.floor(t / 60), t % 60)),\n        duration: fromM != null ? durationLabel(locale, t - fromM) : undefined,\n        disabled: isTimeDisabled?.(v) ?? false,\n      });\n    }\n    return out;\n  }, [fromM, minM, maxM, interval, info, locale, isTimeDisabled]);\n  const exact = slots.some((s) => s.value === value) ? value : null;\n\n  // Opening centers the chosen time, or the nearest one when the typed time is off the grid.\n  useEffect(() => {\n    if (!open) return;\n    const frame = requestAnimationFrame(() => {\n      const list = listRef.current;\n      if (!list || !slots.length) return;\n      const now = new Date();\n      const target = current ?? now.getHours() * 60 + now.getMinutes();\n      const nearest = slots.reduce((a, b) => (Math.abs(parse(b.value)! - target) < Math.abs(parse(a.value)! - target) ? b : a));\n      const el = list.querySelector<HTMLElement>(`[data-time=\"${nearest.value}\"]`);\n      if (el) list.scrollTop = el.offsetTop - list.clientHeight / 2 + el.offsetHeight / 2;\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [open, current, slots]);\n\n  function commit(next: Draft, r: Roll) {\n    setDraft(next);\n    setRoll(r);\n    const v = valueFrom(next, cycle);\n    setSynced({ value: v, cycle });\n    if (v !== value) setValue(v);\n  }\n\n  const setSeg = (d: Draft, seg: Seg, n: number | null): Draft =>\n    seg === \"hour\" ? { ...d, h: n } : seg === \"minute\" ? { ...d, m: n } : { ...d, p: n as 0 | 1 | null };\n  const getSeg = (seg: Seg) => (seg === \"hour\" ? draft.h : seg === \"minute\" ? draft.m : draft.p);\n\n  function focusSeg(seg: Seg, dir: 1 | -1) {\n    const next = order[order.indexOf(seg) + dir];\n    if (next) segRefs.current[next]?.focus();\n  }\n\n  function stepSeg(seg: Seg, dir: 1 | -1, big = false) {\n    const r = range(seg, cycle);\n    const now = getSeg(seg);\n    let n: number;\n    if (now == null) {\n      // An empty segment starts from the current time, which is almost always nearer than midnight.\n      const d = new Date();\n      n = seg === \"hour\" ? (cycle === 12 ? d.getHours() % 12 || 12 : d.getHours()) : seg === \"minute\" ? 0 : d.getHours() >= 12 ? 1 : 0;\n    } else if (seg === \"minute\") {\n      const by = big ? 10 : step;\n      n = by > 1 ? (dir > 0 ? Math.floor(now / by) * by + by : Math.ceil(now / by) * by - by) : now + dir;\n      n = wrap(n, r);\n    } else n = wrap(now + dir, r);\n    let next = setSeg(draft, seg, n);\n    // Filling the hour in a 12-hour field fills AM/PM too, so a time is never stuck half-done.\n    if (seg === \"hour\" && cycle === 12 && next.p == null) next = { ...next, p: new Date().getHours() >= 12 ? 1 : 0 };\n    commit(next, { source: \"step\", seg, trend: dir });\n  }\n\n  function typeDigit(seg: Seg, digit: string) {\n    const [lo, hi] = range(seg, cycle);\n    let text = typed?.seg === seg ? typed.text + digit : digit;\n    if (text.length > 2 || Number(text) > hi) text = digit;\n    const n = Number(text);\n    if (n >= lo && n <= hi) {\n      let next = setSeg(draft, seg, n);\n      // A typed 12-hour time guesses its half of the day from working hours: 1–6 is afternoon, 7–11 morning.\n      if (seg === \"hour\" && cycle === 12 && next.p == null) next = { ...next, p: n === 12 || n < 7 ? 1 : 0 };\n      commit(next, { source: \"type\" });\n    }\n    // Stay for a second digit only while one could still fit; otherwise move on like a native field.\n    if (text.length < 2 && n * 10 <= hi) setTyped({ seg, text });\n    else {\n      setTyped(null);\n      focusSeg(seg, 1);\n    }\n  }\n\n  const onSegKeyDown = (seg: Seg) => (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const k = e.key;\n    if (k === \"ArrowLeft\") focusSeg(seg, -1);\n    else if (k === \"ArrowRight\") focusSeg(seg, 1);\n    else if (e.altKey && k === \"ArrowDown\") setOpen(true);\n    else if (readOnly) return;\n    else if (k === \"ArrowUp\" || k === \"ArrowDown\") stepSeg(seg, k === \"ArrowUp\" ? 1 : -1);\n    else if (k === \"PageUp\" || k === \"PageDown\") stepSeg(seg, k === \"PageUp\" ? 1 : -1, seg === \"minute\");\n    else if (k === \"Home\" || k === \"End\") {\n      const [lo, hi] = range(seg, cycle);\n      commit(setSeg(draft, seg, k === \"Home\" ? lo : hi), { source: \"type\" });\n    } else if (/^\\d$/.test(k) && seg !== \"period\") typeDigit(seg, k);\n    else if (k === \"Backspace\" || k === \"Delete\") {\n      if (getSeg(seg) == null) focusSeg(seg, -1);\n      else commit(setSeg(draft, seg, null), { source: \"type\" });\n      setTyped(null);\n    } else if (seg === \"period\" && k.length === 1 && /\\p{L}/u.test(k)) {\n      const key = k.toLowerCase();\n      const [am, pm] = info.periods.map((p) => p.toLowerCase());\n      const p = key === \"a\" || (am[0] === key && pm[0] !== key) ? 0 : key === \"p\" || (pm[0] === key && am[0] !== key) ? 1 : null;\n      if (p == null) return;\n      commit(setSeg(draft, seg, p), { source: \"step\", seg, trend: p ? 1 : -1 });\n    } else if ([\":\", \".\", \" \", \"h\"].includes(k) && seg !== order.at(-1)) focusSeg(seg, 1);\n    else return;\n    // A handled key belongs to the field, like it would to an input: page shortcuts must not see it.\n    e.preventDefault();\n    e.stopPropagation();\n  };\n\n  const minutesForClock =\n    draft.h != null && draft.m != null ? (cycle === 24 ? draft.h * 60 + draft.m : ((draft.h % 12) + (draft.p ?? 0) * 12) * 60 + draft.m) : null;\n  const rollFor = (seg: Seg) => {\n    const animated = !reduce && roll.source !== \"type\" && (roll.source === \"pick\" || roll.seg === seg);\n    return { animated, trend: roll.source === \"step\" && roll.seg === seg ? roll.trend : undefined };\n  };\n\n  const segClass = cn(\n    \"relative inline-flex h-[22px] select-none items-center justify-center rounded-[4px] px-[3px] outline-none\",\n    \"caret-transparent transition-colors duration-75\",\n    readOnly ? \"focus:bg-hover focus:text-fg\" : \"focus:bg-fg focus:text-frame\",\n    !disabled && \"hover:bg-fg/6\",\n  );\n\n  return (\n    <div\n      ref={fieldRef}\n      role=\"group\"\n      data-size={size}\n      data-state={open ? \"open\" : \"closed\"}\n      data-invalid={invalid || undefined}\n      data-disabled={disabled || undefined}\n      data-readonly={readOnly || undefined}\n      onPointerDown={(e) => {\n        onPointerDown?.(e);\n        // Touch has no keyboard for the segments, so a tap on the field opens the list instead.\n        if (e.pointerType !== \"touch\" || disabled || readOnly || e.defaultPrevented) return;\n        if ((e.target as HTMLElement).closest(\"[data-slot=time-trigger]\")) return;\n        e.preventDefault();\n        setOpen(true);\n      }}\n      className={cn(\n        \"group/time inline-flex shrink-0 items-center gap-1 rounded-lg border border-line-2 bg-raised text-fg shadow-[var(--shadow)]\",\n        \"transition-[border-color,box-shadow] duration-150 ease-out\",\n        \"hover:border-fg-4 focus-within:border-fg-4 focus-within:ring-2 focus-within:ring-fg/10 data-[state=open]:border-fg-4\",\n        \"data-invalid:border-danger/70 data-invalid:hover:border-danger data-invalid:focus-within:border-danger data-invalid:focus-within:ring-danger/15\",\n        \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n        size === \"sm\" ? \"h-7 pl-1.5 pr-0.5 text-[12.5px]\" : \"h-8 pl-2 pr-1 text-[13px] pointer-coarse:h-10\",\n        className,\n      )}\n      {...rest}\n    >\n      {/* Every piece is laid over an invisible widest time, so the field never changes width. */}\n      <div className=\"grid tabular tracking-[-0.003em]\">\n        <div aria-hidden className=\"invisible col-start-1 row-start-1 flex items-center whitespace-pre\">\n          {info.pieces.map((p, i) =>\n            p.kind === \"lit\" ? (\n              <span key={i}>{p.text}</span>\n            ) : (\n              <span key={i} className=\"px-[3px]\">\n                {p.seg === \"period\" ? (info.periods[0].length >= info.periods[1].length ? info.periods[0] : info.periods[1]) : \"88\"}\n              </span>\n            ),\n          )}\n        </div>\n        <div className=\"col-start-1 row-start-1 flex items-center whitespace-pre\">\n          {info.pieces.map((p, i) => {\n            if (p.kind === \"lit\")\n              return (\n                <span key={i} aria-hidden className=\"text-fg-3\">\n                  {p.text}\n                </span>\n              );\n            const seg = p.seg;\n            const n = getSeg(seg);\n            const [lo, hi] = range(seg, cycle);\n            const pending = typed?.seg === seg && Number(typed.text) !== n ? typed.text : null;\n            const text = n == null ? null : seg === \"period\" ? info.periods[n] : seg === \"minute\" || info.padHour ? pad(n) : String(n);\n            const r = rollFor(seg);\n            return (\n              <div\n                key={seg}\n                ref={(el) => {\n                  segRefs.current[seg] = el;\n                }}\n                role=\"spinbutton\"\n                tabIndex={disabled ? -1 : 0}\n                aria-label={info.fieldNames[seg]}\n                aria-valuemin={lo}\n                aria-valuemax={hi}\n                aria-valuenow={n ?? undefined}\n                aria-valuetext={text ?? \"Empty\"}\n                aria-invalid={invalid || undefined}\n                aria-readonly={readOnly || undefined}\n                aria-disabled={disabled || undefined}\n                data-seg={seg}\n                data-placeholder={n == null ? \"\" : undefined}\n                onKeyDown={onSegKeyDown(seg)}\n                onFocus={() => setTyped(null)}\n                onBlur={() => setTyped(null)}\n                className={segClass}\n              >\n                {pending != null ? (\n                  <span>{pending}</span>\n                ) : n == null ? (\n                  <span className={cn(\"text-fg-4\", !readOnly && \"in-focus:text-frame/60\")}>––</span>\n                ) : seg === \"period\" ? (\n                  <PeriodLabel labels={info.periods} index={n as 0 | 1} animated={r.animated} />\n                ) : (\n                  <NumberFlow\n                    aria-hidden\n                    value={n}\n                    locales={locale}\n                    format={{ minimumIntegerDigits: seg === \"minute\" || info.padHour ? 2 : 1, useGrouping: false }}\n                    digits={{ 1: { max: seg === \"minute\" ? 5 : cycle === 12 ? 1 : 2 } }}\n                    trend={r.trend}\n                    animated={r.animated}\n                    transformTiming={rollTiming}\n                    spinTiming={rollTiming}\n                    opacityTiming={{ duration: 140, easing: \"ease-out\" }}\n                  />\n                )}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <Select.Root\n        value={exact}\n        onValueChange={(v) => {\n          if (typeof v === \"string\") commit(draftFrom(v, cycle), { source: \"pick\" });\n        }}\n        open={open}\n        onOpenChange={setOpen}\n        modal={false}\n        disabled={disabled}\n        readOnly={readOnly}\n      >\n        <Select.Trigger\n          data-slot=\"time-trigger\"\n          aria-label={listLabel}\n          className={cn(\n            \"relative ml-auto grid shrink-0 place-items-center rounded-md text-fg-3 outline-none\",\n            \"transition-[background-color,color,scale] duration-150 ease-out active:scale-[0.92] active:duration-75\",\n            \"hover:bg-fg/6 hover:text-fg data-popup-open:bg-fg/6 data-popup-open:text-fg\",\n            \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n            \"after:absolute after:-inset-1.5 after:content-[''] data-readonly:hidden\",\n            size === \"sm\" ? \"size-5\" : \"size-6\",\n          )}\n        >\n          <ClockGlyph minutes={minutesForClock} size={size === \"sm\" ? 14 : 16} />\n        </Select.Trigger>\n        <Select.Portal>\n          <Select.Positioner anchor={fieldRef} side=\"bottom\" align=\"start\" sideOffset={6} alignItemWithTrigger={false} className=\"z-(--z-popover) outline-none\">\n            <Select.Popup\n              className={cn(\n                \"min-w-[max(var(--anchor-width),10rem)] origin-(--transform-origin) overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n                \"transition-[opacity,scale,translate,filter] duration-200 ease-out-expo\",\n                \"data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-starting-style:blur-[2px] data-starting-style:-translate-y-1 data-[side=top]:data-starting-style:translate-y-1\",\n                \"data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-150\",\n                \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:blur-none\",\n              )}\n            >\n              <Select.List ref={listRef} className=\"relative max-h-[min(15.5rem,var(--available-height))] overflow-y-auto overscroll-contain p-1 outline-none\">\n                {slots.length === 0 && <div className=\"px-2 py-1.5 text-[12.5px] text-fg-3\">No times in this range</div>}\n                {slots.map((s) => (\n                  <Select.Item\n                    key={s.value}\n                    value={s.value}\n                    label={s.label}\n                    disabled={s.disabled}\n                    data-time={s.value}\n                    className={cn(\n                      \"flex h-8 cursor-default select-none items-center gap-2 rounded-lg pl-1.5 pr-2.5 text-[13px] text-fg-2 outline-none tabular pointer-coarse:h-10\",\n                      \"data-highlighted:bg-fg/6 data-highlighted:text-fg data-selected:text-fg data-disabled:text-fg-4\",\n                    )}\n                  >\n                    <span className=\"grid size-4 shrink-0 place-items-center\">\n                      <Select.ItemIndicator>\n                        <DrawnCheck reduce={!!reduce} />\n                      </Select.ItemIndicator>\n                    </span>\n                    <Select.ItemText className=\"whitespace-nowrap\">{s.label}</Select.ItemText>\n                    {s.duration && <span className=\"ml-auto whitespace-nowrap pl-3 text-[12px] text-fg-4\">{s.duration}</span>}\n                  </Select.Item>\n                ))}\n              </Select.List>\n            </Select.Popup>\n          </Select.Positioner>\n        </Select.Portal>\n      </Select.Root>\n\n      {name && <input type=\"hidden\" name={name} value={value ?? \"\"} disabled={disabled} />}\n    </div>\n  );\n}\n\n/** AM/PM swaps inside a box as wide as the longer label, sliding the way the value moved. */\nfunction PeriodLabel({ labels, index, animated }: { labels: [string, string]; index: 0 | 1; animated: boolean }) {\n  const dir = index === 1 ? 1 : -1;\n  return (\n    <span className=\"relative grid overflow-hidden\">\n      {labels.map((l) => (\n        <span key={l} aria-hidden className=\"invisible col-start-1 row-start-1\">\n          {l}\n        </span>\n      ))}\n      <AnimatePresence initial={false} custom={dir}>\n        <motion.span\n          key={index}\n          custom={dir}\n          className=\"col-start-1 row-start-1 text-center\"\n          variants={{\n            enter: (d: number) => ({ y: d * 10, opacity: 0, filter: \"blur(2px)\" }),\n            center: { y: 0, opacity: 1, filter: \"blur(0px)\" },\n            exit: (d: number) => ({ y: d * -10, opacity: 0, filter: \"blur(2px)\" }),\n          }}\n          initial={animated ? \"enter\" : false}\n          animate=\"center\"\n          exit={animated ? \"exit\" : { opacity: 0, transition: { duration: 0 } }}\n          transition={{ duration: 0.2, ease: ease.out }}\n        >\n          {labels[index]}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/** A clock whose hands point at the chosen time and sweep the short way round when it changes. */\nfunction ClockGlyph({ minutes, size }: { minutes: number | null; size: number }) {\n  const hourAngle = minutes == null ? 300 : ((minutes / 60) % 12) * 30;\n  const minuteAngle = minutes == null ? 60 : (minutes % 60) * 6;\n  const [hands, setHands] = useState({ key: minutes, h: hourAngle, m: minuteAngle });\n  if (hands.key !== minutes) {\n    const turn = (from: number, to: number) => from + ((((to - from) % 360) + 540) % 360) - 180;\n    setHands({ key: minutes, h: turn(hands.h, hourAngle), m: turn(hands.m, minuteAngle) });\n  }\n  const hand = \"origin-[8px_8px] [transform-box:view-box] transition-transform duration-[420ms] ease-in-out-quart\";\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinecap=\"round\" aria-hidden>\n      <circle cx=\"8\" cy=\"8\" r=\"5.75\" />\n      <path d=\"M8 8V5.9\" className={hand} style={{ transform: `rotate(${hands.h}deg)` }} />\n      <path d=\"M8 8V3.9\" className={hand} style={{ transform: `rotate(${hands.m}deg)` }} />\n    </svg>\n  );\n}\n\nfunction DrawnCheck({ reduce }: { reduce: boolean }) {\n  return (\n    <svg\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.6}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      <motion.path\n        d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n        initial={reduce ? false : { pathLength: 0 }}\n        animate={{ pathLength: 1 }}\n        transition={{ duration: 0.28, ease: ease.out, delay: 0.04 }}\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/time-picker"
}
