{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timezone-select",
  "type": "registry:ui",
  "title": "Timezone Select",
  "description": "Every IANA zone with live local time, searchable by city, name or offset.",
  "categories": [
    "scheduling"
  ],
  "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/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/timezone-select.tsx",
      "content": "\"use client\";\nimport { Combobox } from \"@base-ui/react/combobox\";\nimport NumberFlow, { NumberFlowGroup } from \"@number-flow/react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, type AnimationPlaybackControls } from \"motion/react\";\nimport { useEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, ChevronsUpDown, Globe, Search } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\n\n/* ------------------------------------------------------------------ */\n/* Zones, from Intl alone.                                             */\n/* ------------------------------------------------------------------ */\n\nexport type TimeZoneInfo = {\n  /** The IANA id, exactly as the engine reports it. */\n  id: string;\n  /** \"Kolkata\", \"New York\", \"UTC\". */\n  city: string;\n  /** \"Argentina\" for America/Argentina/Salta; empty when there's no middle part. */\n  area: string;\n  /** \"India Standard Time\", \"Pacific Time\". */\n  name: string;\n  region: string;\n  /** Minutes ahead of UTC right now. */\n  offset: number;\n  /** Everything a search can match, folded to lowercase ASCII. */\n  haystack: string;\n};\n\nconst REGIONS: [string, string][] = [\n  [\"America\", \"Americas\"],\n  [\"Europe\", \"Europe\"],\n  [\"Africa\", \"Africa\"],\n  [\"Asia\", \"Asia\"],\n  [\"Australia\", \"Australia\"],\n  [\"Pacific\", \"Pacific\"],\n  [\"Atlantic\", \"Atlantic\"],\n  [\"Indian\", \"Indian Ocean\"],\n  [\"Antarctica\", \"Antarctica\"],\n  [\"Arctic\", \"Arctic\"],\n  [\"Other\", \"Other\"],\n];\n// Old spellings some engines still hand back, shown by their current names and found by both.\nconst RENAMED: Record<string, string> = { Calcutta: \"Kolkata\", Saigon: \"Ho Chi Minh City\", Katmandu: \"Kathmandu\", Kiev: \"Kyiv\", Rangoon: \"Yangon\", Godthab: \"Nuuk\", Ashkhabad: \"Ashgabat\", Chongqing: \"Chongqing\", Faeroe: \"Faroe\" };\n// What people type that no zone is called.\nconst ALIASES: Record<string, string> = {\n  \"America/Los_Angeles\": \"pst pdt pt pacific san francisco seattle california\",\n  \"America/New_York\": \"est edt et eastern boston washington miami toronto\",\n  \"America/Chicago\": \"cst cdt ct central dallas houston\",\n  \"America/Denver\": \"mst mdt mt mountain\",\n  \"America/Phoenix\": \"arizona\",\n  \"Europe/London\": \"gmt bst uk britain england\",\n  \"Europe/Paris\": \"cet cest france\",\n  \"Europe/Berlin\": \"cet cest germany\",\n  \"Asia/Kolkata\": \"ist india mumbai delhi bangalore bengaluru\",\n  \"Asia/Calcutta\": \"ist india mumbai delhi bangalore bengaluru\",\n  \"Asia/Tokyo\": \"jst japan\",\n  \"Asia/Shanghai\": \"china beijing\",\n  \"Asia/Singapore\": \"sgt\",\n  \"Asia/Hong_Kong\": \"hkt\",\n  \"Australia/Sydney\": \"aest aedt melbourne canberra\",\n  \"Asia/Dubai\": \"gst uae\",\n  \"America/Sao_Paulo\": \"brt brazil\",\n  UTC: \"utc gmt zulu coordinated universal\",\n};\nconst FALLBACK = \"America/Los_Angeles America/Denver America/Chicago America/New_York America/Sao_Paulo Europe/London Europe/Paris Europe/Berlin Europe/Kyiv Africa/Lagos Africa/Johannesburg Asia/Dubai Asia/Kolkata Asia/Singapore Asia/Shanghai Asia/Tokyo Australia/Sydney Pacific/Auckland\".split(\" \");\n\nexport const fold = (s: string) =>\n  s\n    .normalize(\"NFD\")\n    .replace(/\\p{M}/gu, \"\")\n    .toLowerCase();\n\nconst nameFmt = new Map<string, Intl.DateTimeFormat>();\nfunction zonePart(at: Date, timeZone: string, style: \"longGeneric\" | \"shortOffset\", locale = \"en-US\") {\n  const key = `${locale}|${timeZone}|${style}`;\n  let f = nameFmt.get(key);\n  if (!f) {\n    f = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: style });\n    nameFmt.set(key, f);\n  }\n  return f.formatToParts(at).find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n}\n\n/** Minutes the zone is ahead of UTC at that instant. */\nexport function zoneOffset(timeZone: string, at = new Date()) {\n  const label = zonePart(at, timeZone, \"shortOffset\");\n  const m = /GMT([+-−])(\\d{1,2})(?::(\\d{2}))?/.exec(label);\n  if (!m) return 0;\n  return (m[1] === \"+\" ? 1 : -1) * (Number(m[2]) * 60 + Number(m[3] ?? 0));\n}\n\n/** \"GMT+5:30\", \"GMT−3\", \"GMT\". A real minus sign, so columns of offsets read cleanly. */\nexport function formatOffset(minutes: number) {\n  if (minutes === 0) return \"GMT\";\n  const a = Math.abs(minutes);\n  return `GMT${minutes > 0 ? \"+\" : \"−\"}${Math.floor(a / 60)}${a % 60 ? `:${String(a % 60).padStart(2, \"0\")}` : \"\"}`;\n}\n\nexport function zoneCity(id: string) {\n  if (id === \"UTC\" || id === \"Etc/UTC\") return \"UTC\";\n  const last = (id.split(\"/\").pop() ?? id).replace(/_/g, \" \");\n  return RENAMED[last.replace(/ /g, \"\")] ?? last;\n}\n\nlet cache: { at: number; zones: TimeZoneInfo[] } | null = null;\n\n/** Every zone the engine knows, with its current offset and generic name. Rebuilt at most once a minute. */\nexport function getTimeZones(now = new Date()): TimeZoneInfo[] {\n  const minute = Math.floor(now.getTime() / 60000);\n  if (cache && cache.at === minute) return cache.zones;\n  let ids: string[];\n  try {\n    ids = (Intl as unknown as { supportedValuesOf: (k: string) => string[] }).supportedValuesOf(\"timeZone\");\n  } catch {\n    ids = FALLBACK;\n  }\n  if (!ids.includes(\"UTC\")) ids = [...ids, \"UTC\"];\n  const zones = ids.map((id) => {\n    const parts = id.split(\"/\");\n    const region = parts.length > 1 && REGIONS.some(([k]) => k === parts[0]) ? parts[0] : \"Other\";\n    const city = zoneCity(id);\n    const area = parts.length > 2 ? parts.slice(1, -1).join(\" / \").replace(/_/g, \" \") : \"\";\n    const name = id === \"UTC\" ? \"Coordinated Universal Time\" : zonePart(now, id, \"longGeneric\");\n    const offset = zoneOffset(id, now);\n    const raw = (parts.pop() ?? \"\").replace(/_/g, \" \");\n    return { id, city, area, name, region, offset, haystack: fold([city, raw, area, name, id, ALIASES[id] ?? \"\"].join(\" \")) };\n  });\n  cache = { at: minute, zones };\n  return zones;\n}\n\n/** Reads \"+5:30\", \"gmt-3\", \"UTC+9\", \"−4\" as an offset in minutes. */\nfunction parseOffset(q: string): number | null {\n  const m = /^(?:utc|gmt)?\\s*([+\\-−])\\s*(\\d{1,2})(?::?(\\d{2}))?$/.exec(q.trim());\n  if (!m) return null;\n  return (m[1] === \"+\" ? 1 : -1) * (Number(m[2]) * 60 + Number(m[3] ?? 0));\n}\n\nfunction rank(zone: TimeZoneInfo, q: string) {\n  const city = fold(zone.city);\n  if (city === q) return 0;\n  if (city.startsWith(q)) return 1;\n  if (zone.haystack.split(/[\\s/]+/).some((w) => w.startsWith(q))) return 2;\n  return 3;\n}\n\nexport function filterTimeZones(zones: TimeZoneInfo[], query: string) {\n  const q = fold(query.trim());\n  if (!q) return zones;\n  const offset = parseOffset(q);\n  if (offset !== null) return zones.filter((z) => z.offset === offset);\n  const terms = q.split(/\\s+/);\n  // Every term has to start a word, so \"new y\" finds New York and not Papua New Guinea.\n  return zones\n    .filter((z) => {\n      const words = z.haystack.split(/[\\s/()-]+/);\n      return terms.every((t) => words.some((w) => w.startsWith(t)));\n    })\n    .map((z) => ({ z, r: rank(z, terms[0]) }))\n    .sort((a, b) => a.r - b.r)\n    .map(({ z }) => z);\n}\n\n/* ------------------------------------------------------------------ */\n/* Browser-only clocks.                                                */\n/* ------------------------------------------------------------------ */\n\n// Ticks on the minute boundary; stops while the tab is hidden and catches up when it returns.\nfunction subscribeMinute(notify: () => void) {\n  let timer = 0;\n  const arm = () => {\n    window.clearTimeout(timer);\n    if (document.visibilityState === \"hidden\") return;\n    timer = window.setTimeout(() => (notify(), arm()), 60000 - (Date.now() % 60000) + 20);\n  };\n  const onVisible = () => (notify(), arm());\n  arm();\n  document.addEventListener(\"visibilitychange\", onVisible);\n  return () => {\n    window.clearTimeout(timer);\n    document.removeEventListener(\"visibilitychange\", onVisible);\n  };\n}\n/** The current minute, live. Null during server render. */\nexport function useMinute() {\n  return useSyncExternalStore(subscribeMinute, () => Math.floor(Date.now() / 60000), () => null);\n}\nconst noop = () => () => {};\n/** The viewer's zone. Null during server render. */\nexport function useDetectedTimeZone() {\n  return useSyncExternalStore(noop, () => Intl.DateTimeFormat().resolvedOptions().timeZone, () => null);\n}\nfunction useLocale(locale?: string) {\n  const browser = useSyncExternalStore(noop, () => navigator.language, () => \"en-US\");\n  return locale ?? browser;\n}\nfunction prefers12h(locale: string) {\n  try {\n    const cycle = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions().hourCycle;\n    return cycle === \"h11\" || cycle === \"h12\";\n  } catch {\n    return false;\n  }\n}\n\nconst timeCache = new Map<string, Intl.DateTimeFormat>();\nfunction timeFormat(locale: string, hour12: boolean, timeZone: string) {\n  const key = `${locale}|${hour12}|${timeZone}`;\n  let f = timeCache.get(key);\n  if (!f) {\n    f = new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", hour12, timeZone });\n    timeCache.set(key, f);\n  }\n  return f;\n}\n\n/** Wall-clock hour, minute and day in a zone. */\nfunction clockIn(timeZone: string, at: Date) {\n  const shifted = new Date(at.getTime() + zoneOffset(timeZone, at) * 60000);\n  return { h: shifted.getUTCHours(), m: shifted.getUTCMinutes(), day: Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()) / 864e5 };\n}\n\n/* ------------------------------------------------------------------ */\n\ntype Group = { value: string; label: string; items: TimeZoneInfo[] };\n\nexport type TimezoneSelectProps = {\n  /** The IANA id, when controlled. */\n  value?: string | null;\n  /** Defaults to the viewer's zone, once it's known. */\n  defaultValue?: string | null;\n  onValueChange?: (timeZone: string) => void;\n  /** Restrict the list, e.g. to the zones your product supports. */\n  timeZones?: string[];\n  locale?: string;\n  hour12?: boolean;\n  /** Pin the viewer's zone at the top of the list. */\n  showDetected?: boolean;\n  label?: React.ReactNode;\n  \"aria-label\"?: string;\n  size?: \"sm\" | \"md\";\n  disabled?: boolean;\n  name?: string;\n  container?: Combobox.Portal.Props[\"container\"];\n  className?: string;\n};\n\nexport function TimezoneSelect({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  timeZones,\n  locale: localeProp,\n  hour12: hour12Prop,\n  showDetected = true,\n  label,\n  \"aria-label\": ariaLabel = \"Time zone\",\n  size = \"md\",\n  disabled,\n  name,\n  container,\n  className,\n}: TimezoneSelectProps) {\n  const reduce = !!useReducedMotion();\n  const minute = useMinute();\n  const detected = useDetectedTimeZone();\n  const locale = useLocale(localeProp);\n  const hour12 = hour12Prop ?? prefers12h(locale);\n  const [inner, setInner] = useState<string | null | undefined>(defaultValue);\n  const current = valueProp !== undefined ? valueProp : (inner ?? detected);\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n\n  const now = useMemo(() => (minute === null ? null : new Date(minute * 60000)), [minute]);\n  // The full list is only built on the client, and only rebuilt when the minute (and so maybe an offset) changes.\n  const zones = useMemo(() => {\n    if (!now) return [];\n    const all = getTimeZones(now);\n    const allowed = timeZones ? new Set(timeZones) : null;\n    return all.filter((z) => !allowed || allowed.has(z.id));\n  }, [now, timeZones]);\n\n  const selected = useMemo(() => zones.find((z) => z.id === current) ?? (current && now ? makeInfo(current, now) : null), [zones, current, now]);\n  const home = useMemo(() => (detected && now ? (zones.find((z) => z.id === detected) ?? makeInfo(detected, now)) : null), [zones, detected, now]);\n\n  const groups = useMemo<Group[]>(() => {\n    const matched = filterTimeZones(zones, query);\n    const searching = !!query.trim();\n    const out: Group[] = [];\n    if (showDetected && home && !searching) out.push({ value: \"detected\", label: \"Your time zone\", items: [home] });\n    for (const [key, title] of REGIONS) {\n      const items = matched.filter((z) => z.region === key);\n      // Browsing: west to east within a region. Searching: best match first.\n      if (!searching) items.sort((a, b) => a.offset - b.offset || a.city.localeCompare(b.city));\n      if (items.length) out.push({ value: key, label: title, items });\n    }\n    // One group of best matches reads better than scattered hits when searching.\n    if (searching) return out.length ? [{ value: \"results\", label: `${matched.length} ${matched.length === 1 ? \"match\" : \"matches\"}`, items: matched }] : [];\n    return out;\n  }, [zones, query, showDetected, home]);\n\n  const choose = (z: TimeZoneInfo | null) => {\n    if (!z) return;\n    if (valueProp === undefined) setInner(z.id);\n    onValueChange?.(z.id);\n  };\n\n  const timeIn = (id: string) => (now ? timeFormat(locale, hour12, id).format(now) : \"\");\n  const homeDay = detected && now ? clockIn(detected, now).day : null;\n  const dayShift = (id: string) => (now && homeDay !== null ? clockIn(id, now).day - homeDay : 0);\n\n  const { setList, y, height, opacity } = useGlide(reduce);\n  const clock = selected && now ? clockIn(selected.id, now) : null;\n\n  return (\n    <Combobox.Root<TimeZoneInfo>\n      items={groups}\n      filteredItems={groups}\n      value={selected}\n      onValueChange={choose}\n      inputValue={query}\n      onInputValueChange={setQuery}\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next);\n        if (!next) setQuery(\"\");\n      }}\n      autoHighlight\n      isItemEqualToValue={(a, b) => a.id === b.id}\n      itemToStringLabel={(z) => z.city}\n      itemToStringValue={(z) => z.id}\n      disabled={disabled}\n      name={name}\n    >\n      <div className={cn(\"flex w-full min-w-0 max-w-[340px] flex-col gap-1.5\", className)}>\n        {label && <Combobox.Label className=\"w-fit cursor-default text-[12.5px] font-medium text-fg-2 select-none\">{label}</Combobox.Label>}\n        <Combobox.Trigger\n          aria-label={label ? undefined : ariaLabel}\n          data-size={size}\n          className={cn(\n            \"group/trigger relative flex w-full min-w-0 items-center border border-line-2 bg-raised text-left text-fg shadow-[var(--shadow)] select-none\",\n            \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n            \"transition-[background-color,border-color,scale] duration-150 ease-out hover:border-fg-4 hover:bg-hover active:scale-[0.985] active:duration-75\",\n            \"data-popup-open:border-fg-4 data-popup-open:bg-hover data-disabled:pointer-events-none data-disabled:opacity-50\",\n            size === \"sm\" ? \"h-7 gap-1.5 rounded-md pr-1.5 pl-2 text-[12.5px]\" : \"h-8 gap-2 rounded-lg pr-2 pl-2.5 text-[13px]\",\n          )}\n        >\n          <Globe size={size === \"sm\" ? 14 : 16} className=\"shrink-0 text-fg-3\" />\n          <span className=\"grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)] overflow-hidden py-1\">\n            <AnimatePresence initial={false}>\n              <motion.span\n                key={selected?.id ?? \"none\"}\n                className=\"col-start-1 row-start-1 flex min-w-0 items-baseline gap-1.5\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8, filter: \"blur(2px)\" }}\n                animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8, filter: \"blur(2px)\" }}\n                transition={{ duration: reduce ? 0.12 : 0.22, ease: ease.out }}\n              >\n                {selected ? (\n                  <>\n                    <span className=\"truncate\">{selected.city}</span>\n                    <span className=\"shrink-0 font-mono text-2xs text-fg-4\">{formatOffset(selected.offset)}</span>\n                  </>\n                ) : (\n                  <span className=\"my-0.5 h-3 w-28 rounded bg-hover\" />\n                )}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n          {/* The clock rolls rather than swaps: a new zone moves the hours, a new minute moves the minutes. */}\n          <span className=\"shrink-0 text-[12.5px] text-fg-3 tabular\" aria-hidden>\n            {clock ? (\n              <NumberFlowGroup>\n                <span className=\"inline-flex items-baseline\">\n                  <NumberFlow value={hour12 ? clock.h % 12 || 12 : clock.h} format={{ minimumIntegerDigits: hour12 ? 1 : 2 }} />\n                  <span>:</span>\n                  <NumberFlow value={clock.m} format={{ minimumIntegerDigits: 2 }} />\n                  {hour12 && <span className=\"ml-1\">{clock.h < 12 ? \"AM\" : \"PM\"}</span>}\n                </span>\n              </NumberFlowGroup>\n            ) : (\n              <span className=\"inline-block h-3 w-10 rounded bg-hover align-middle\" />\n            )}\n          </span>\n          <ChevronsUpDown size={size === \"sm\" ? 14 : 16} className=\"shrink-0 text-fg-3 transition-colors duration-150 group-hover/trigger:text-fg-2\" />\n        </Combobox.Trigger>\n      </div>\n\n      <Combobox.Portal container={container}>\n        <Combobox.Positioner align=\"start\" sideOffset={6} collisionPadding={8} className=\"z-(--z-popover) outline-none\">\n          <Combobox.Popup\n            aria-label=\"Choose a time zone\"\n            className={cn(\n              \"flex w-[max(var(--anchor-width),22rem)] max-w-(--available-width) origin-(--transform-origin) flex-col overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n              \"transition-[opacity,scale] duration-160 ease-out-expo data-ending-style:duration-100 data-ending-style:ease-out\",\n              \"data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0\",\n              \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n            )}\n          >\n            <div className=\"relative flex items-center border-b border-line\">\n              <Search size={14} className=\"pointer-events-none absolute left-3 text-fg-3\" />\n              <Combobox.Input\n                placeholder=\"City, zone or offset like +5:30\"\n                aria-label=\"Search time zones\"\n                className=\"h-10 w-full bg-transparent pr-3 pl-8 text-base text-fg outline-none placeholder:text-fg-4 sm:text-[13px]\"\n              />\n            </div>\n            <Combobox.Empty className=\"empty:hidden\">\n              <div className=\"flex flex-col gap-0.5 px-3 py-6 text-center\">\n                <p className=\"truncate text-[13px] text-fg-2\">No time zones match “{query.trim()}”</p>\n                <p className=\"text-[12px] text-fg-3\">Try a city, a country’s zone name or an offset.</p>\n              </div>\n            </Combobox.Empty>\n            <Combobox.List\n              ref={setList}\n              className=\"relative max-h-[min(calc(var(--available-height)-2.5rem),20rem)] scroll-pt-8 scroll-pb-1 overflow-y-auto overscroll-contain px-1 pb-1 outline-none empty:hidden\"\n            >\n              <motion.div aria-hidden style={{ y, height, opacity }} className=\"pointer-events-none absolute inset-x-1 top-0 rounded-lg bg-line\" />\n              {groups.map((group) => (\n                <Combobox.Group key={group.value} items={group.items} className=\"block\">\n                  <Combobox.GroupLabel className=\"sticky top-0 z-[2] flex h-8 items-end bg-raised px-2 pb-1.5 font-mono text-2xs tracking-[0.08em] text-fg-3 uppercase select-none\">\n                    {group.label}\n                  </Combobox.GroupLabel>\n                  <Combobox.Collection>\n                    {(z: TimeZoneInfo) => {\n                      const shift = dayShift(z.id);\n                      return (\n                        <Combobox.Item\n                          key={`${group.value}:${z.id}`}\n                          value={z}\n                          className=\"group/item relative z-[1] flex h-8 cursor-default scroll-my-1 items-center gap-2 rounded-lg pr-2 pl-2 text-[13px] outline-none select-none pointer-coarse:h-10\"\n                        >\n                          <span className=\"grid size-4 shrink-0 place-items-center\">\n                            <Combobox.ItemIndicator>\n                              <Check size={14} />\n                            </Combobox.ItemIndicator>\n                          </span>\n                          <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n                            <span className=\"shrink-0 truncate text-fg\">\n                              <Highlight text={z.city} query={query} />\n                            </span>\n                            <span className=\"min-w-0 truncate text-[12px] text-fg-3\">{z.area ? `${z.area} · ${z.name}` : z.name}</span>\n                          </span>\n                          <span className=\"w-[4.25rem] shrink-0 text-right font-mono text-2xs text-fg-4\">{formatOffset(z.offset)}</span>\n                          <span className=\"flex w-[4.75rem] shrink-0 items-baseline justify-end gap-1 text-[12.5px] text-fg-2 tabular\">\n                            {timeIn(z.id)}\n                            {shift !== 0 && (\n                              <sup className=\"font-mono text-[9.5px] text-fg-3\" aria-label={shift > 0 ? \"next day\" : \"previous day\"}>\n                                {shift > 0 ? \"+1\" : \"−1\"}\n                              </sup>\n                            )}\n                          </span>\n                        </Combobox.Item>\n                      );\n                    }}\n                  </Combobox.Collection>\n                </Combobox.Group>\n              ))}\n            </Combobox.List>\n          </Combobox.Popup>\n        </Combobox.Positioner>\n      </Combobox.Portal>\n    </Combobox.Root>\n  );\n}\n\nfunction makeInfo(id: string, now: Date): TimeZoneInfo {\n  const parts = id.split(\"/\");\n  return { id, city: zoneCity(id), area: \"\", name: zonePart(now, id, \"longGeneric\"), region: parts.length > 1 ? parts[0] : \"Other\", offset: zoneOffset(id, now), haystack: fold(id) };\n}\n\n/** The matched run of a city in the primary color, the rest a step quieter. */\nfunction Highlight({ text, query }: { text: string; query: string }) {\n  const q = fold(query.trim());\n  if (!q || parseOffset(q) !== null) return <>{text}</>;\n  // The whole query when the city holds it (\"new y\" in New York), else its first word.\n  const run = fold(text).includes(q) ? q : q.split(/\\s+/)[0];\n  const at = fold(text).indexOf(run);\n  if (at < 0) return <>{text}</>;\n  const end = at + run.length;\n  return (\n    <>\n      <span className=\"text-fg-2\">{text.slice(0, at)}</span>\n      <mark className=\"bg-transparent font-medium text-fg\">{text.slice(at, end)}</mark>\n      <span className=\"text-fg-2\">{text.slice(end)}</span>\n    </>\n  );\n}\n\n// One highlight for the list: glides after the pointer, jumps for arrow keys and typing.\nfunction useGlide(reduce: boolean) {\n  const [list, setList] = useState<HTMLDivElement | null>(null);\n  const y = useMotionValue(0);\n  const height = useMotionValue(32);\n  const opacity = useMotionValue(0);\n\n  useEffect(() => {\n    if (!list) return;\n    let keyboard = false;\n    let shown = false;\n    let running: AnimationPlaybackControls[] = [];\n    const stop = () => {\n      running.forEach((c) => c.stop());\n      running = [];\n    };\n    const place = () => {\n      const el = list.querySelector<HTMLElement>(\"[data-highlighted]\");\n      stop();\n      if (!el) {\n        shown = false;\n        running.push(animate(opacity, 0, { duration: reduce ? 0 : 0.12 }));\n        return;\n      }\n      let top = 0;\n      for (let n: HTMLElement | null = el; n && n !== list; n = n.offsetParent as HTMLElement | null) top += n.offsetTop;\n      if (!shown || keyboard || reduce) {\n        y.jump(top);\n        height.jump(el.offsetHeight);\n      } else {\n        running.push(animate(y, top, spring.follow), animate(height, el.offsetHeight, spring.follow));\n      }\n      opacity.jump(1);\n      shown = true;\n    };\n    const onKey = () => (keyboard = true);\n    const onPointer = () => (keyboard = false);\n    const observer = new MutationObserver(place);\n    observer.observe(list, { subtree: true, childList: true, attributes: true, attributeFilter: [\"data-highlighted\"] });\n    document.addEventListener(\"keydown\", onKey, true);\n    list.addEventListener(\"pointermove\", onPointer);\n    place();\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"keydown\", onKey, true);\n      list.removeEventListener(\"pointermove\", onPointer);\n      stop();\n    };\n  }, [list, reduce, y, height, opacity]);\n\n  return { setList, y, height, opacity };\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/timezone-select"
}
