{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"time-zone-select","type":"registry:ui","title":"Time Zone Select","description":"A time zone picker: offsets, cities and names in the reader's language, searchable.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/combobox.json"],"files":[{"path":"registry/dashral/time-zone-select.tsx","type":"registry:file","target":"components/dashral/time-zone-select.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { Combobox, type ComboboxOption } from \"./combobox\"\n\ntype TimeZoneSelectProps = Omit<React.ComponentProps<typeof Combobox>, \"options\" | \"value\" | \"defaultValue\" | \"onValueChange\"> & {\n  /** The time zone, an IANA name (\"Asia/Jakarta\"), controlled. */\n  value?: string\n  /** The time zone first picked. \"local\" for the reader's own (from the browser), once the page has loaded. */\n  defaultValue?: string | \"local\"\n  onValueChange?: (timeZone: string) => void\n  /** Only these time zones, in place of all the browser knows. */\n  timeZones?: string[]\n  /** BCP 47 locale for the names (\"Western Indonesia Time\", \"Waktu Indonesia Barat\"); the LocaleProvider's by default. */\n  locale?: string\n}\n\n/** Minutes ahead of UTC for a time zone right now (DST included), e.g. 420 for Asia/Jakarta. */\nfunction offsetOf(timeZone: string, at: Date) {\n  const parts = new Intl.DateTimeFormat(\"en-US\", { timeZone, hourCycle: \"h23\", year: \"numeric\", month: \"numeric\", day: \"numeric\", hour: \"numeric\", minute: \"numeric\" }).formatToParts(at)\n  const get = (type: string) => Number(parts.find((p) => p.type === type)?.value)\n  const local = Date.UTC(get(\"year\"), get(\"month\") - 1, get(\"day\"), get(\"hour\"), get(\"minute\"))\n  return Math.round((local - Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate(), at.getUTCHours(), at.getUTCMinutes())) / 60_000)\n}\n\n/** \"GMT+07:00\", \"GMT−03:30\", \"GMT+00:00\": the same width for every zone, so the list lines up. */\nfunction gmt(offset: number) {\n  const sign = offset < 0 ? \"−\" : \"+\"\n  const abs = Math.abs(offset)\n  return `GMT${sign}${String(Math.floor(abs / 60)).padStart(2, \"0\")}:${String(abs % 60).padStart(2, \"0\")}`\n}\n\n/** Every time zone the browser knows, or a short list where it can't say (older engines). */\nfunction allTimeZones(): string[] {\n  try {\n    return (Intl as unknown as { supportedValuesOf: (key: string) => string[] }).supportedValuesOf(\"timeZone\")\n  } catch {\n    return [\"UTC\", \"America/Los_Angeles\", \"America/New_York\", \"Europe/London\", \"Europe/Paris\", \"Asia/Dubai\", \"Asia/Kolkata\", \"Asia/Jakarta\", \"Asia/Singapore\", \"Asia/Tokyo\", \"Australia/Sydney\"]\n  }\n}\n\n/**\n * A time zone picker, in one tag: `<TimeZoneSelect label=\"Time zone\" defaultValue=\"local\" />`. Each zone reads\n * \"GMT+07:00 Jakarta\" with its name in the reader's language under it (\"Western Indonesia Time\"), sorted by offset,\n * and is found by city, region or name. The offsets are today's, daylight saving included. Built on Combobox, so it\n * takes the same label, description, error and classNames.\n */\nfunction TimeZoneSelect({ value, defaultValue, onValueChange, timeZones, locale: localeProp, placeholder, searchPlaceholder, ...props }: TimeZoneSelectProps) {\n  const { t, locale: contextLocale } = useLocale()\n  const locale = localeProp ?? contextLocale\n  const [internal, setInternal] = React.useState(defaultValue === \"local\" ? \"\" : (defaultValue ?? \"\"))\n  // \"local\" can only be known in the browser: filled in once it has loaded, so the server and browser agree first.\n  React.useEffect(() => {\n    if (defaultValue === \"local\" && value === undefined) setInternal(Intl.DateTimeFormat().resolvedOptions().timeZone)\n  }, [defaultValue, value])\n  const current = value ?? internal\n\n  const options = React.useMemo<ComboboxOption[]>(() => {\n    const now = new Date()\n    /** The zone's name in the locale, \"Western Indonesia Time\"; \"\" where the engine can't say. */\n    const nameOf = (timeZone: string) => {\n      try {\n        return new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: \"long\" }).formatToParts(now).find((p) => p.type === \"timeZoneName\")?.value ?? \"\"\n      } catch {\n        return \"\"\n      }\n    }\n    const zones = timeZones ?? allTimeZones()\n    return zones\n      .map((zone) => {\n        let offset = 0\n        try {\n          offset = offsetOf(zone, now)\n        } catch {\n          return null\n        }\n        // \"America/Argentina/Buenos_Aires\" → \"Buenos Aires\"; the region stays searchable through the description.\n        const city = zone.split(\"/\").at(-1)!.replace(/_/g, \" \")\n        const name = nameOf(zone)\n        return { zone, offset, option: { value: zone, label: `${gmt(offset)} ${city}`, description: [name, zone.includes(\"/\") ? zone.split(\"/\")[0]!.replace(/_/g, \" \") : \"\"].filter(Boolean).join(\" · \") } }\n      })\n      .filter((entry) => entry !== null)\n      .sort((a, b) => a.offset - b.offset || a.option.label.localeCompare(b.option.label, locale))\n      .map((entry) => entry.option)\n  }, [timeZones, locale])\n\n  return (\n    <Combobox\n      {...props}\n      placeholder={placeholder ?? t(\"pickTimeZone\")}\n      searchPlaceholder={searchPlaceholder ?? t(\"searchTimeZones\")}\n      options={options}\n      value={current}\n      onValueChange={(next) => {\n        // Picking the current zone again would clear it; a time zone setting always keeps one.\n        const zone = next || current\n        if (value === undefined) setInternal(zone)\n        if (zone !== current) onValueChange?.(zone)\n      }}\n    />\n  )\n}\n\nexport { TimeZoneSelect, type TimeZoneSelectProps }\n"}]}