{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"time-picker","type":"registry:ui","title":"Time Picker","description":"A time of day you type or pick – a list every few minutes with a gliding highlight, 12 or 24 hours by locale, min and max, and typing that understands 930, 9:30 pm or 21.30","dependencies":[],"files":[{"path":"registry/time-picker/time-picker.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from \"react\";\n\n/*\n * Time Picker – a single self-contained file.\n * A time of day: a field in the Text Field's look you can type into (\"9\", \"930\", \"9:30 pm\", \"21.30\") or open for a\n * list of times every few minutes – the chosen one has a check, a soft highlight glides to where the pointer or the\n * arrow keys are. 12 or 24 hours follow the locale unless you set them. Times outside min / max aren't offered, and\n * anything that isn't a time puts the last good one back. The value is \"HH:mm\" (24 h), like a native time input.\n * The list sits in the top layer, so a card's overflow never cuts it. The styles are injected below, so no extra\n * files are needed.\n */\n\nexport type TimePickerProps = {\n  /** The chosen time as \"HH:mm\" (24 h) – makes it controlled; null shows the placeholder */\n  value?: string | null;\n  /** Starting time when uncontrolled */\n  defaultValue?: string | null;\n  /** Called with \"HH:mm\", or null when the field is cleared */\n  onChange?: (value: string | null) => void;\n  /** Minutes between the times in the list – typing can still set any minute */\n  step?: number;\n  /** The earliest time, \"HH:mm\" – the list starts here */\n  min?: string;\n  /** The latest time, \"HH:mm\" */\n  max?: string;\n  /** 12 (9:30 PM) or 24 (21:30) hours – defaults to the locale's */\n  hourCycle?: 12 | 24;\n  /** BCP 47 locale for how times are written – defaults to the browser's */\n  locale?: string;\n  /** Shown while nothing is chosen – defaults to an example time in the right format */\n  placeholder?: string;\n  /** Shown above the field and tied to it */\n  label?: ReactNode;\n  /** A quiet line under the field – the error takes its place while there is one */\n  hint?: ReactNode;\n  /** Marks the field invalid and shows this under it, in place of the hint */\n  error?: ReactNode;\n  /** \"Optional\" next to the label */\n  optional?: boolean;\n  /** sm 28 · md 32 · lg 36 high */\n  size?: \"sm\" | \"md\" | \"lg\";\n  disabled?: boolean;\n  /** Sends \"HH:mm\" with a form, through a hidden input */\n  name?: string;\n  required?: boolean;\n  id?: string;\n  className?: string;\n  \"aria-label\"?: string;\n  ref?: Ref<HTMLInputElement>;\n};\n\n/* ---------- Times as minutes after midnight ---------- */\n\nconst DAY = 24 * 60;\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n/** \"HH:mm\" (seconds allowed) → minutes, or null */\nconst fromValue = (v?: string | null) => {\n  const m = typeof v === \"string\" ? v.trim().match(/^(\\d{1,2}):(\\d{2})(?::\\d{2}(?:\\.\\d+)?)?$/) : null;\n  if (!m) return null;\n  const h = Number(m[1]);\n  const mi = Number(m[2]);\n  return h < 24 && mi < 60 ? h * 60 + mi : null;\n};\nconst toValue = (t: number) => `${pad(Math.floor(t / 60))}:${pad(t % 60)}`;\nconst norm = (s: string) => s.toLowerCase().replace(/[.\\s\\u00a0\\u202f]/g, \"\");\n/** The zeros of the digit sets people type times in (Arabic-Indic, Persian, Devanagari, Bengali, Thai, full-width …) */\nconst ZEROS = [0x660, 0x6f0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xe50, 0xed0, 0xf20, 0x1040, 0x17e0, 0x1810, 0xff10];\n/** \"٩:٣٠\" → \"9:30\" */\nconst latinDigits = (s: string) =>\n  s.replace(/\\p{Nd}/gu, (c) => {\n    const cp = c.codePointAt(0)!;\n    const zero = ZEROS.find((z) => cp >= z && cp <= z + 9);\n    return zero == null ? c : String(cp - zero);\n  });\n\n/** A locale the browser knows, or undefined (the browser's own) */\nfunction safeLocale(locale?: string) {\n  if (!locale) return undefined;\n  try {\n    return Intl.getCanonicalLocales(locale)[0];\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * What people type: \"9\", \"930\", \"9:30\", \"09.30\", \"9h30\", \"9:30 pm\", \"9p\", \"21:30\", \"2130\" – and the locale's own day\n * periods, before or after (\"下午3:30\", \"오후 3:30\"). Without a period the hour is read as 24 h.\n */\nfunction parseTime(text: string, am: Set<string>, pm: Set<string>): number | null {\n  const t = latinDigits(text).trim().toLowerCase();\n  if (!t) return null;\n  let pre = \"\";\n  let post = \"\";\n  let h: number;\n  let mi = 0;\n  const a = t.match(/^(\\D*?)\\s*(\\d{1,2})(?:\\s*[:.h]\\s*(\\d{2}))?\\s*(\\D*)$/);\n  const b = a ? null : t.match(/^(\\D*?)\\s*(\\d{3,4})\\s*(\\D*)$/);\n  if (a) {\n    [, pre, , , post] = a;\n    h = Number(a[2]);\n    mi = a[3] ? Number(a[3]) : 0;\n  } else if (b) {\n    [, pre, , post] = b;\n    h = Number(b[2].slice(0, -2));\n    mi = Number(b[2].slice(-2));\n  } else return null;\n  const before = norm(pre);\n  const after = norm(post);\n  if (before && after) return null;\n  const word = before || after;\n  const period = !word ? null : am.has(word) ? \"am\" : pm.has(word) ? \"pm\" : undefined;\n  if (period === undefined || mi > 59) return null;\n  if (period) {\n    if (h < 1 || h > 12) return null;\n    h = (h % 12) + (period === \"pm\" ? 12 : 0);\n  } else if (h === 24 && mi === 0) h = 0;\n  else if (h > 23) return null;\n  return h * 60 + mi;\n}\n\n/* ---------- Picker ---------- */\n\nexport function TimePicker({\n  value: valueProp,\n  defaultValue = null,\n  onChange,\n  step: stepProp = 15,\n  min,\n  max,\n  hourCycle,\n  locale,\n  placeholder,\n  label,\n  hint,\n  error,\n  optional = false,\n  size = \"md\",\n  disabled = false,\n  name,\n  required,\n  id,\n  className,\n  \"aria-label\": ariaLabel,\n  ref,\n}: TimePickerProps) {\n  const autoId = useId();\n  const inputId = id ?? `${autoId}-input`;\n  const listId = `${autoId}-list`;\n  const labelId = `${autoId}-label`;\n  const messageId = `${autoId}-message`;\n  const optionId = (i: number) => `${autoId}-option-${i}`;\n\n  const loc = safeLocale(locale);\n  const { format, am, pm } = useMemo(() => {\n    const twelve = hourCycle === 12 || hourCycle === 24 ? hourCycle === 12 : /h1[12]/.test(new Intl.DateTimeFormat(loc, { hour: \"numeric\" }).resolvedOptions().hourCycle ?? \"\");\n    const f = new Intl.DateTimeFormat(loc, { hour: twelve ? \"numeric\" : \"2-digit\", minute: \"2-digit\", hourCycle: twelve ? \"h12\" : \"h23\" });\n    // The locale's own words for before and after noon, so \"下午3:30\" or \"3:30 nachm.\" can be typed too\n    const period = (h: number) => {\n      const part = new Intl.DateTimeFormat(loc, { hour: \"numeric\", hourCycle: \"h12\" }).formatToParts(new Date(2000, 0, 1, h)).find((p) => p.type === \"dayPeriod\");\n      return part ? norm(part.value) : \"\";\n    };\n    return {\n      format: (t: number) => f.format(new Date(2000, 0, 1, Math.floor(t / 60), t % 60)),\n      am: new Set([\"a\", \"am\", period(1)].filter(Boolean)),\n      pm: new Set([\"p\", \"pm\", period(13)].filter(Boolean)),\n    };\n  }, [loc, hourCycle]);\n\n  const step = Number.isFinite(stepProp) ? Math.min(DAY, Math.max(1, Math.round(stepProp))) : 15;\n  const minT = fromValue(min) ?? 0;\n  const maxRaw = fromValue(max);\n  const maxT = maxRaw != null && maxRaw >= minT ? maxRaw : DAY - 1;\n  const times = useMemo(() => {\n    const out: number[] = [];\n    for (let t = minT; t <= maxT; t += step) out.push(t);\n    return out;\n  }, [minT, maxT, step]);\n\n  const [own, setOwn] = useState<number | null>(() => fromValue(defaultValue));\n  const value = valueProp !== undefined ? fromValue(valueProp) : own;\n  const shown = value != null ? format(value) : \"\";\n\n  const [text, setText] = useState(shown);\n  const [editing, setEditing] = useState(false);\n  const [open, setOpen] = useState(false);\n  const [active, setActive] = useState(-1);\n  const [glow, setGlow] = useState<{ top: number; height: number } | null>(null);\n  const navigated = useRef(false);\n  const center = useRef(false);\n\n  const input = useRef<HTMLInputElement | null>(null);\n  const box = useRef<HTMLDivElement | null>(null);\n  const list = useRef<HTMLDivElement | null>(null);\n\n  // While not typing, the field shows the chosen time in the locale's format\n  const fieldText = editing ? text : shown;\n  const invalid = Boolean(error);\n  const message = shows(error) ? error : shows(hint) ? hint : null;\n  const scale = SIZES.includes(size) ? size : \"md\";\n\n  /** The list row closest to a time (the first one at or after it, else the last) */\n  const nearest = (t: number) => {\n    const i = times.findIndex((x) => x >= t);\n    return i < 0 ? times.length - 1 : i;\n  };\n  const now = () => {\n    const d = new Date();\n    return d.getHours() * 60 + d.getMinutes();\n  };\n\n  const show = (at?: number) => {\n    if (disabled || !times.length) return;\n    const exact = value != null ? times.indexOf(value) : -1;\n    setActive(at ?? (exact >= 0 ? exact : nearest(value ?? now())));\n    navigated.current = false;\n    center.current = true;\n    setOpen(true);\n  };\n  const hide = () => {\n    setOpen(false);\n    setGlow(null);\n  };\n  const commit = (t: number | null) => {\n    if (valueProp === undefined) setOwn(t);\n    if (t !== value) onChange?.(t == null ? null : toValue(t));\n    setEditing(false);\n  };\n  const pick = (i: number) => {\n    if (i < 0 || i >= times.length) return;\n    commit(times[i]);\n    hide();\n  };\n  /** The typed text: a time inside min / max is kept, an empty field clears, anything else puts the last time back */\n  const settle = () => {\n    if (!editing) return;\n    const t = text.trim() ? parseTime(text, am, pm) : null;\n    if (!text.trim()) commit(null);\n    else if (t != null && t >= minT && t <= maxT) commit(t);\n    else setEditing(false);\n  };\n\n  const type = (next: string) => {\n    setText(next);\n    setEditing(true);\n    navigated.current = false;\n    if (!times.length) return;\n    // The list follows what is typed: the time it reads as, or the first row that starts with it\n    const t = next.trim() ? parseTime(next, am, pm) : null;\n    const q = norm(latinDigits(next));\n    const hit = t != null ? nearest(t) : q ? times.findIndex((x) => norm(latinDigits(format(x))).startsWith(q)) : -1;\n    if (!open) {\n      center.current = true;\n      setOpen(true);\n    }\n    if (hit >= 0) setActive(hit);\n  };\n\n  // Into the top layer, under the field (or above it when there is more room there); it follows on scroll\n  useLayoutEffect(() => {\n    const el = list.current;\n    if (!open || !el) return;\n    if (\"showPopover\" in el && !el.matches(\":popover-open\")) el.showPopover();\n    const update = (e?: Event) => {\n      // The list's own scrolling doesn't move it\n      if (e?.type === \"scroll\" && e.target instanceof Node && el.contains(e.target)) return;\n      if (box.current) place(el, box.current);\n    };\n    update();\n    window.addEventListener(\"resize\", update);\n    window.addEventListener(\"scroll\", update, true);\n    return () => {\n      window.removeEventListener(\"resize\", update);\n      window.removeEventListener(\"scroll\", update, true);\n      if (\"hidePopover\" in el && el.isConnected && el.matches(\":popover-open\")) el.hidePopover();\n    };\n  }, [open]);\n\n  // The highlight glides to the active time; the list keeps it in view (centred when it opens)\n  useLayoutEffect(() => {\n    if (!open || active < 0) return;\n    const el = list.current;\n    const row = el?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n    if (!el || !row) return;\n    setGlow({ top: row.offsetTop, height: row.offsetHeight });\n    if (center.current) {\n      el.scrollTop = row.offsetTop - (el.clientHeight - row.offsetHeight) / 2;\n      center.current = false;\n    } else row.scrollIntoView({ block: \"nearest\" });\n  }, [active, open]);\n\n  // A press anywhere else closes it; what was typed is settled when the field loses the focus\n  useEffect(() => {\n    if (!open) return;\n    const down = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (box.current?.contains(target) || list.current?.contains(target)) return;\n      hide();\n    };\n    document.addEventListener(\"pointerdown\", down, true);\n    return () => document.removeEventListener(\"pointerdown\", down, true);\n  }, [open]);\n\n  const move = (by: number) => {\n    if (!open) return show();\n    navigated.current = true;\n    setActive((a) => Math.min(times.length - 1, Math.max(0, (a < 0 ? 0 : a) + by)));\n  };\n\n  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return;\n    const page = Math.max(1, Math.round(60 / step));\n    switch (e.key) {\n      case \"ArrowDown\":\n        e.preventDefault();\n        if (e.altKey && !open) show();\n        else move(1);\n        break;\n      case \"ArrowUp\":\n        e.preventDefault();\n        move(-1);\n        break;\n      case \"PageDown\":\n        if (!open) return;\n        e.preventDefault();\n        move(page);\n        break;\n      case \"PageUp\":\n        if (!open) return;\n        e.preventDefault();\n        move(-page);\n        break;\n      case \"Enter\":\n        if (open && navigated.current && active >= 0) {\n          e.preventDefault();\n          pick(active);\n        } else if (editing || open) {\n          e.preventDefault();\n          settle();\n          hide();\n        }\n        break;\n      case \"Escape\":\n        if (open) {\n          e.preventDefault();\n          e.stopPropagation();\n          hide();\n        } else if (editing) {\n          e.preventDefault();\n          setEditing(false);\n        }\n        break;\n      case \"Tab\":\n        hide();\n        break;\n    }\n  };\n\n  const ph = placeholder ?? format(9 * 60);\n\n  return (\n    <div\n      className={[\"ui-tpk\", className].filter(Boolean).join(\" \")}\n      data-size={scale}\n      data-open={open || undefined}\n      data-invalid={invalid || undefined}\n      data-disabled={disabled || undefined}\n    >\n      {/* React 19 hoists this into <head> once, no matter how many pickers render */}\n      <style href=\"ui-time-picker\" precedence=\"default\">\n        {css}\n      </style>\n      {label != null && (\n        <div className=\"ui-tpk__head\">\n          <label className=\"ui-tpk__label\" id={labelId} htmlFor={inputId}>\n            {label}\n          </label>\n          {optional && <span className=\"ui-tpk__optional\">Optional</span>}\n        </div>\n      )}\n      <div ref={box} className=\"ui-tpk__box\" onClick={() => input.current?.focus()}>\n        <span className=\"ui-tpk__icon\" aria-hidden=\"true\">\n          <ClockIcon />\n        </span>\n        <input\n          ref={(el) => {\n            input.current = el;\n            if (typeof ref === \"function\") ref(el);\n            else if (ref) ref.current = el;\n          }}\n          id={inputId}\n          className=\"ui-tpk__input\"\n          type=\"text\"\n          role=\"combobox\"\n          inputMode=\"text\"\n          autoComplete=\"off\"\n          autoCorrect=\"off\"\n          spellCheck={false}\n          aria-autocomplete=\"list\"\n          aria-expanded={open}\n          aria-controls={listId}\n          aria-activedescendant={open && active >= 0 ? optionId(active) : undefined}\n          aria-label={ariaLabel}\n          aria-describedby={message ? messageId : undefined}\n          aria-invalid={invalid || undefined}\n          aria-required={required || undefined}\n          placeholder={ph}\n          disabled={disabled}\n          value={fieldText}\n          onChange={(e) => type(e.target.value)}\n          onKeyDown={onKeyDown}\n          onPointerDown={() => !open && show()}\n          onBlur={() => {\n            settle();\n            hide();\n          }}\n        />\n        <button\n          type=\"button\"\n          className=\"ui-tpk__toggle\"\n          tabIndex={-1}\n          aria-label={open ? \"Hide times\" : \"Show times\"}\n          disabled={disabled}\n          onPointerDown={(e) => e.preventDefault()}\n          onClick={(e) => {\n            e.stopPropagation();\n            input.current?.focus();\n            if (open) hide();\n            else show();\n          }}\n        >\n          <ChevronIcon />\n        </button>\n      </div>\n      {name && <input type=\"hidden\" name={name} value={value != null ? toValue(value) : \"\"} />}\n      {open && (\n        <div\n          ref={list}\n          id={listId}\n          className=\"ui-tpk__list\"\n          role=\"listbox\"\n          aria-labelledby={label != null ? labelId : undefined}\n          aria-label={label == null ? ariaLabel ?? \"Times\" : undefined}\n          popover=\"manual\"\n        >\n          {glow && <span className=\"ui-tpk__glow\" aria-hidden=\"true\" style={{ transform: `translateY(${glow.top}px)`, height: glow.height }} />}\n          {times.map((t, i) => (\n            <div\n              key={t}\n              id={optionId(i)}\n              className=\"ui-tpk__option\"\n              role=\"option\"\n              data-index={i}\n              aria-selected={t === value}\n              data-active={i === active || undefined}\n              onPointerMove={() => {\n                navigated.current = true;\n                if (i !== active) setActive(i);\n              }}\n              onPointerDown={(e) => e.preventDefault()}\n              onClick={() => pick(i)}\n            >\n              <span className=\"ui-tpk__time\">{format(t)}</span>\n              <span className=\"ui-tpk__check\" aria-hidden=\"true\">\n                {t === value && <CheckIcon />}\n              </span>\n            </div>\n          ))}\n        </div>\n      )}\n      {message && (\n        <p className=\"ui-tpk__message\" id={messageId} key={shows(error) ? \"error\" : \"hint\"} aria-live={shows(error) ? \"polite\" : undefined}>\n          {message}\n        </p>\n      )}\n    </div>\n  );\n}\n\nconst SIZES = [\"sm\", \"md\", \"lg\"];\n/** Text or an element – not true, false, null or an empty string */\nconst shows = (v: ReactNode) => v != null && typeof v !== \"boolean\" && v !== \"\";\n\n/** Under the field (or above it when there is more room there), as wide as the field, always inside the window */\nfunction place(el: HTMLElement, field: HTMLElement) {\n  const r = field.getBoundingClientRect();\n  const gap = 4;\n  const edge = 8;\n  const vw = document.documentElement.clientWidth || window.innerWidth;\n  const vh = window.innerHeight;\n  const width = Math.min(Math.max(r.width, 120), vw - 2 * edge);\n  el.style.width = `${width}px`;\n  const want = Math.min(el.scrollHeight, 264);\n  const below = vh - r.bottom - gap - edge;\n  const above = r.top - gap - edge;\n  const up = below < Math.min(want, 160) && above > below;\n  const room = Math.max(120, Math.min(264, up ? above : below));\n  el.style.maxHeight = `${room}px`;\n  const h = Math.min(want, room);\n  el.style.left = `${Math.round(Math.min(Math.max(edge, r.left), vw - edge - width))}px`;\n  el.style.top = `${Math.round(up ? r.top - gap - h : r.bottom + gap)}px`;\n  el.toggleAttribute(\"data-up\", up);\n}\n\n/* Icons: Hugeicons Free (MIT, © Hugeicons, hugeicons.com) – Clock 01, Arrow Down 01, Tick 02 */\nfunction ClockIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      <circle cx=\"12\" cy=\"12\" r=\"10\" />\n      <path d=\"M12 8V12L14 14\" />\n    </svg>\n  );\n}\nfunction ChevronIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      <path d=\"M18 9C18 9 13.58 15 12 15C10.42 15 6 9 6 9\" />\n    </svg>\n  );\n}\nfunction CheckIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      <path className=\"ui-tpk__tick\" d=\"M5 14L8.5 17.5L19 6.5\" pathLength={1} />\n    </svg>\n  );\n}\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-tpk (see Theming in the docs) */\n.ui-tpk {\n  --ui-tpk-title: var(--aui-ink, #171717);\n  --ui-tpk-ink: var(--aui-ink, #171717);\n  --ui-tpk-subtle: var(--aui-muted, #8f8f8f);\n  --ui-tpk-faint: var(--aui-placeholder, #b5b5b5);\n  --ui-tpk-icon: var(--aui-muted, #8f8f8f);\n  --ui-tpk-surface: var(--aui-surface, #ffffff);\n  --ui-tpk-menu: var(--aui-overlay, var(--aui-surface, #ffffff));\n  --ui-tpk-line: var(--aui-border, #e6e6e8);\n  --ui-tpk-edge: var(--aui-border-strong, #d4d4d8);\n  --ui-tpk-edge-focus: var(--aui-border-strong, #b4b4bb);\n  --ui-tpk-glow: #f4f4f5;\n  --ui-tpk-readonly: #fafafa;\n  --ui-tpk-shadow-menu: var(--aui-shadow-popover, 0 0 0 1px rgba(0, 0, 0, 0.06), 0 18px 40px -12px rgba(0, 0, 0, 0.18), 0 4px 12px -4px rgba(0, 0, 0, 0.06));\n  --ui-tpk-danger: var(--aui-danger, #c4320a);\n  --ui-tpk-danger-edge: #ff8a7a;\n  --ui-tpk-danger-wash: color-mix(in srgb, var(--aui-red, #f04438) 4%, transparent);\n  --ui-tpk-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-tpk-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n\n  --h: 36px;\n  --px: 12px;\n  --fs: 13.5px;\n  --radius: var(--aui-radius, 10px);\n\n  display: flex;\n  flex-direction: column;\n  gap: 7px;\n  min-width: 0;\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13.5px;\n  line-height: 20px;\n  color: var(--ui-tpk-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-tpk[data-size=\"sm\"] { --h: 32px; --px: 10px; --fs: 13px; --radius: calc(var(--aui-radius, 10px) - 1px); }\n.ui-tpk[data-size=\"lg\"] { --h: 40px; --px: 14px; --fs: 14px; --radius: calc(var(--aui-radius, 10px) + 2px); }\n/* Touch screens: a comfortable tap height, and 16px text so iOS doesn't zoom in on focus */\n@media (pointer: coarse) {\n  .ui-tpk { --fs: 16px; }\n  .ui-tpk[data-size=\"sm\"] { --h: 36px; }\n  .ui-tpk[data-size=\"md\"] { --h: 40px; }\n  .ui-tpk[data-size=\"lg\"] { --h: 44px; }\n}\n.ui-tpk *, .ui-tpk *::before, .ui-tpk *::after { box-sizing: border-box; }\n\n.ui-tpk__head { display: flex; align-items: baseline; gap: 6px; min-width: 0; padding: 0 2px; }\n.ui-tpk__label { min-width: 0; font-size: 13px; font-weight: 500; color: var(--ui-tpk-title); overflow-wrap: anywhere; cursor: default; }\n.ui-tpk__optional { flex: none; font-size: 12px; line-height: 16px; color: var(--ui-tpk-subtle); }\n\n/* The field: white with a 1px gray edge; hover and typing only darken that edge – no ring on a text field */\n.ui-tpk__box {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  height: var(--h);\n  min-width: 0;\n  padding: 0 calc(var(--px) - 6px) 0 var(--px);\n  border-radius: var(--radius);\n  background: var(--ui-tpk-surface);\n  box-shadow: 0 0 0 1px var(--ui-tpk-line), 0 1px 2px rgba(0, 0, 0, 0.04);\n  cursor: text;\n  transition: box-shadow 160ms ease, background-color 160ms ease;\n}\n.ui-tpk__box:hover { box-shadow: 0 0 0 1px var(--ui-tpk-edge), 0 1px 2px rgba(0, 0, 0, 0.04); }\n.ui-tpk__box:focus-within, .ui-tpk[data-open] .ui-tpk__box { box-shadow: 0 0 0 1px var(--ui-tpk-edge-focus), 0 1px 2px rgba(0, 0, 0, 0.04); }\n.ui-tpk[data-invalid] .ui-tpk__box { background: var(--ui-tpk-danger-wash); box-shadow: 0 0 0 1px var(--ui-tpk-danger-edge), 0 1px 2px rgba(0, 0, 0, 0.04); }\n.ui-tpk[data-disabled] .ui-tpk__box { background: var(--ui-tpk-readonly); box-shadow: 0 0 0 1px var(--ui-tpk-line); cursor: not-allowed; }\n.ui-tpk[data-disabled] { opacity: 0.55; }\n.ui-tpk__icon { display: grid; flex: none; place-items: center; width: 16px; height: 16px; color: var(--ui-tpk-icon); }\n.ui-tpk__icon svg { display: block; }\n.ui-tpk__input {\n  flex: 1;\n  width: 100%;\n  min-width: 0;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  outline: none;\n  background: none;\n  font: inherit;\n  font-size: var(--fs);\n  font-variant-numeric: tabular-nums;\n  color: var(--ui-tpk-ink);\n  text-overflow: ellipsis;\n}\n.ui-tpk__input::placeholder { color: var(--ui-tpk-faint); opacity: 1; }\n.ui-tpk__input:disabled { cursor: not-allowed; }\n.ui-tpk__toggle {\n  display: grid;\n  flex: none;\n  place-items: center;\n  width: 26px;\n  height: 26px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  border-radius: 7px;\n  background: none;\n  color: var(--ui-tpk-icon);\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n  transition: background-color 140ms ease, color 140ms ease;\n}\n.ui-tpk__toggle svg { display: block; transition: transform 240ms var(--ui-tpk-ease); }\n.ui-tpk__toggle:hover:not(:disabled) { background: var(--ui-tpk-glow); color: var(--ui-tpk-ink); }\n.ui-tpk__toggle:focus-visible { outline: 2px solid var(--ui-tpk-focus); outline-offset: -2px; }\n.ui-tpk__toggle:disabled { cursor: not-allowed; }\n.ui-tpk[data-open] .ui-tpk__toggle svg { transform: rotate(180deg); }\n\n/* The list: a white card in the top layer, at the field's edge */\n.ui-tpk__list {\n  position: fixed;\n  inset: auto;\n  z-index: 1000;\n  margin: 0;\n  padding: 5px;\n  border: 0;\n  border-radius: 14px;\n  background: var(--ui-tpk-menu);\n  box-shadow: var(--ui-tpk-shadow-menu);\n  color: var(--ui-tpk-ink);\n  font: inherit;\n  overflow-y: auto;\n  overscroll-behavior: contain;\n  scrollbar-width: thin;\n  transform-origin: top center;\n  animation: ui-tpk-in 200ms var(--ui-tpk-ease) both;\n}\n.ui-tpk__list[data-up] { transform-origin: bottom center; animation-name: ui-tpk-in-up; }\n.ui-tpk__list::backdrop { display: none; }\n\n/* One soft highlight that glides between the rows */\n.ui-tpk__glow {\n  position: absolute;\n  top: 0;\n  left: 5px;\n  right: 5px;\n  border-radius: 9px;\n  background: var(--ui-tpk-glow);\n  pointer-events: none;\n  transition: transform 180ms var(--ui-tpk-ease), height 180ms var(--ui-tpk-ease);\n}\n\n.ui-tpk__option {\n  position: relative;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  height: 34px;\n  padding: 0 10px;\n  border-radius: 9px;\n  font-size: 13.5px;\n  line-height: 20px;\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n  cursor: pointer;\n  user-select: none;\n}\n.ui-tpk__time { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n.ui-tpk__option[aria-selected=\"true\"] .ui-tpk__time { font-weight: 500; }\n.ui-tpk__check { display: grid; flex: none; place-items: center; width: 16px; height: 16px; color: var(--ui-tpk-ink); }\n.ui-tpk__check svg { display: block; }\n.ui-tpk__tick { stroke-dasharray: 1; animation: ui-tpk-draw 280ms 40ms var(--ui-tpk-ease) both; }\n@media (pointer: coarse) {\n  .ui-tpk__option { height: 40px; font-size: 15px; }\n  .ui-tpk__toggle { width: 34px; height: 34px; }\n}\n\n.ui-tpk__message { margin: 0; padding: 0 2px; font-size: 12.5px; line-height: 17px; color: var(--ui-tpk-subtle); overflow-wrap: anywhere; animation: ui-tpk-message 260ms var(--ui-tpk-ease); }\n.ui-tpk[data-invalid] .ui-tpk__message { color: var(--ui-tpk-danger); }\n\n@keyframes ui-tpk-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }\n@keyframes ui-tpk-in-up { from { opacity: 0; transform: translateY(4px) scale(0.98); } }\n@keyframes ui-tpk-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n@keyframes ui-tpk-message { from { opacity: 0; transform: translateY(-3px); filter: blur(2px); } }\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-tpk *, .ui-tpk *::before, .ui-tpk *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-tpk {\n  --ui-tpk-title: var(--aui-ink, #ededed);\n  --ui-tpk-ink: var(--aui-ink, #ededed);\n  --ui-tpk-subtle: var(--aui-muted, #8b8b92);\n  --ui-tpk-faint: var(--aui-placeholder, #5c5c63);\n  --ui-tpk-icon: var(--aui-muted, #8b8b92);\n  --ui-tpk-surface: var(--aui-surface, #19191b);\n  --ui-tpk-menu: var(--aui-overlay, var(--aui-surface, #19191b));\n  --ui-tpk-line: var(--aui-border, #2a2a2e);\n  --ui-tpk-edge: var(--aui-border-strong, #3a3a40);\n  --ui-tpk-edge-focus: var(--aui-border-strong, #4e4e55);\n  --ui-tpk-glow: #26262a;\n  --ui-tpk-readonly: #151517;\n  --ui-tpk-shadow-menu: var(--aui-shadow-popover, 0 0 0 1px rgba(255, 255, 255, 0.06), 0 18px 40px -12px rgba(0, 0, 0, 0.6));\n  --ui-tpk-danger: var(--aui-danger, #ff8a7a);\n  --ui-tpk-danger-edge: color-mix(in srgb, var(--aui-red, #ff6b5f) 45%, transparent);\n  --ui-tpk-danger-wash: color-mix(in srgb, var(--aui-red, #ff6b5f) 6%, transparent);\n  --ui-tpk-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  color-scheme: dark;\n}\n`;\n"}]}