{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "z-timezone-select",
  "title": "Z Timezone Select",
  "dependencies": [
    "@heroicons/react@2.2.0"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://ui.orchid.ai/r/orchid-foundation.json",
    "https://ui.orchid.ai/r/orchid-theme.json",
    "https://ui.orchid.ai/r/z-popover.json",
    "https://ui.orchid.ai/r/z-scroll-area.json"
  ],
  "files": [
    {
      "path": "registry/ui/orchid/spectrum/molecules/z-timezone-select/z-timezone-select.tsx",
      "content": "\"use client\";\nimport { GlobeAltIcon } from '@heroicons/react/16/solid';\nimport { useEffect, useRef, useState } from 'react';\nimport ZPopover from \"../../atoms/z-popover/z-popover\";\nimport { ZScrollArea } from \"../../atoms/z-scroll-area/z-scroll-area\";\nimport { cn } from \"../../../lib/ui-utils\";\ntype TimezoneOption = {\n    value: string;\n    label: string;\n    city: string;\n    offsetMinutes: number;\n    offsetLabel: string;\n};\ntype ZTimezoneSelectProps = {\n    value: string;\n    onChange: (timezone: string) => void;\n    className?: string;\n};\nexport default function ZTimezoneSelect({ value, onChange, className }: ZTimezoneSelectProps) {\n    const [open, setOpen] = useState(false);\n    const [search, setSearch] = useState('');\n    const [highlightedIndex, setHighlightedIndex] = useState(0);\n    const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());\n    const curatedOptions = getCuratedOptions();\n    const { filtered, fallbackStart } = filterOptions(search, curatedOptions);\n    const selectedOption = curatedOptions.find((opt) => opt.value === value) ?? buildOption(value);\n    const displayLabel = `${selectedOption.offsetLabel} · ${selectedOption.city}`;\n    useEffect(() => {\n        if (open) {\n            const selectedIndex = filtered.findIndex((opt) => opt.value === value);\n            setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0);\n            requestAnimationFrame(() => {\n                const el = itemRefs.current.get(selectedIndex >= 0 ? selectedIndex : 0);\n                el?.scrollIntoView({ block: 'nearest' });\n            });\n        }\n    }, [open]);\n    useEffect(() => {\n        const el = itemRefs.current.get(highlightedIndex);\n        el?.scrollIntoView({ block: 'nearest' });\n    }, [highlightedIndex]);\n    function handleKeyDown(e: React.KeyboardEvent) {\n        if (filtered.length === 0)\n            return;\n        switch (e.key) {\n            case 'ArrowDown':\n                e.preventDefault();\n                setHighlightedIndex((prev) => (prev + 1) % filtered.length);\n                break;\n            case 'ArrowUp':\n                e.preventDefault();\n                setHighlightedIndex((prev) => (prev - 1 + filtered.length) % filtered.length);\n                break;\n            case 'Enter':\n                e.preventDefault();\n                selectOption(filtered[highlightedIndex]);\n                break;\n            case 'Escape':\n                e.preventDefault();\n                setOpen(false);\n                setSearch('');\n                break;\n        }\n    }\n    function selectOption(option: TimezoneOption | undefined) {\n        if (!option)\n            return;\n        onChange(option.value);\n        setOpen(false);\n        setSearch('');\n    }\n    return (<ZPopover open={open} onOpenChange={setOpen} align=\"start\" collisionAvoidance={{ side: 'flip' }}>\n      <ZPopover.Trigger>\n        <button type=\"button\" className={cn('text-neutral-subtle border-neutral bg-surface inline-flex h-8 items-center gap-1.5 rounded-lg border px-3 text-sm', className)}>\n          <GlobeAltIcon className=\"size-4\"/>\n          <span className=\"truncate\">{displayLabel}</span>\n        </button>\n      </ZPopover.Trigger>\n      <ZPopover.Content>\n        <div className=\"flex w-72 flex-col\">\n          <input type=\"text\" placeholder=\"Search timezone...\" value={search} onChange={(e) => {\n            setSearch(e.target.value);\n            setHighlightedIndex(0);\n        }} onKeyDown={handleKeyDown} className=\"text-copy border-neutral bg-surface border-b px-3 py-2 text-sm outline-none\" autoFocus/>\n          <ZScrollArea className=\"h-60\">\n            <div className=\"flex flex-col py-1\">\n              {filtered.map((option, index) => (<div key={option.value}>\n                  {index === fallbackStart && (<div className=\"text-neutral-subtle border-neutral border-t px-3 py-1.5 text-xs\">\n                      Other timezones\n                    </div>)}\n                  <button ref={(el) => {\n                if (el)\n                    itemRefs.current.set(index, el);\n                else\n                    itemRefs.current.delete(index);\n            }} type=\"button\" onClick={() => selectOption(option)} className={cn('text-copy flex w-full items-center justify-between px-3 py-1.5 text-left text-sm', index === highlightedIndex\n                ? 'bg-surface-subtle'\n                : 'hover:bg-surface-subtle', option.value === value && 'font-medium')}>\n                    <span className=\"truncate\">{option.city}</span>\n                    <span className=\"text-neutral-subtle ml-2 shrink-0 text-xs\">\n                      {option.offsetLabel}\n                    </span>\n                  </button>\n                </div>))}\n            </div>\n          </ZScrollArea>\n        </div>\n      </ZPopover.Content>\n    </ZPopover>);\n}\nfunction getTimezoneOffset(tz: string): {\n    offsetMinutes: number;\n    offsetLabel: string;\n} {\n    const now = new Date();\n    const parts = new Intl.DateTimeFormat('en-US', {\n        timeZone: tz,\n        timeZoneName: 'longOffset',\n    }).formatToParts(now);\n    const raw = parts.find((p) => p.type === 'timeZoneName')?.value ?? 'GMT';\n    if (raw === 'GMT')\n        return { offsetMinutes: 0, offsetLabel: 'UTC+0' };\n    const match = raw.match(/GMT([+-])(\\d{1,2})(?::(\\d{2}))?/);\n    if (!match)\n        return { offsetMinutes: 0, offsetLabel: 'UTC+0' };\n    const sign = match[1] === '+' ? 1 : -1;\n    const hours = parseInt(match[2], 10);\n    const mins = parseInt(match[3] ?? '0', 10);\n    const offsetMinutes = sign * (hours * 60 + mins);\n    const offsetLabel = mins === 0\n        ? `UTC${match[1]}${hours}`\n        : `UTC${match[1]}${hours}:${String(mins).padStart(2, '0')}`;\n    return { offsetMinutes, offsetLabel };\n}\nfunction cityName(tz: string): string {\n    const parts = tz.split('/');\n    return (parts[parts.length - 1] ?? tz).replace(/_/g, ' ');\n}\nfunction buildOption(tz: string): TimezoneOption {\n    const { offsetMinutes, offsetLabel } = getTimezoneOffset(tz);\n    const city = cityName(tz);\n    return {\n        value: tz,\n        city,\n        offsetMinutes,\n        offsetLabel,\n        label: `${offsetLabel} · ${city}`,\n    };\n}\nlet cachedCurated: TimezoneOption[] | null = null;\nfunction getCuratedOptions(): TimezoneOption[] {\n    if (!cachedCurated) {\n        cachedCurated = CURATED_TIMEZONES.map(buildOption).sort((a, b) => a.offsetMinutes - b.offsetMinutes);\n    }\n    return cachedCurated;\n}\nlet cachedAllOptions: TimezoneOption[] | null = null;\nfunction getAllOptions(): TimezoneOption[] {\n    if (!cachedAllOptions) {\n        const curatedSet = new Set(CURATED_TIMEZONES);\n        cachedAllOptions = Intl.supportedValuesOf('timeZone')\n            .filter((tz) => !curatedSet.has(tz))\n            .map(buildOption)\n            .sort((a, b) => a.offsetMinutes - b.offsetMinutes);\n    }\n    return cachedAllOptions;\n}\nfunction matchesQuery(option: TimezoneOption, lower: string): boolean {\n    return (option.city.toLowerCase().includes(lower) ||\n        option.value.toLowerCase().includes(lower) ||\n        option.offsetLabel.toLowerCase().includes(lower) ||\n        option.label.toLowerCase().includes(lower));\n}\nfunction filterOptions(search: string, curatedOptions: TimezoneOption[]): {\n    filtered: TimezoneOption[];\n    fallbackStart: number;\n} {\n    if (!search)\n        return { filtered: curatedOptions, fallbackStart: -1 };\n    const lower = search.toLowerCase();\n    const curatedMatches = curatedOptions.filter((opt) => matchesQuery(opt, lower));\n    if (curatedMatches.length >= 3) {\n        return { filtered: curatedMatches, fallbackStart: -1 };\n    }\n    const allMatches = getAllOptions().filter((opt) => matchesQuery(opt, lower));\n    if (allMatches.length === 0) {\n        return { filtered: curatedMatches, fallbackStart: -1 };\n    }\n    return {\n        filtered: [...curatedMatches, ...allMatches],\n        fallbackStart: curatedMatches.length,\n    };\n}\nconst CURATED_TIMEZONES = [\n    'Pacific/Midway',\n    'Pacific/Honolulu',\n    'Pacific/Tahiti',\n    'America/Anchorage',\n    'America/Los_Angeles',\n    'America/Vancouver',\n    'America/Denver',\n    'America/Phoenix',\n    'America/Chicago',\n    'America/Mexico_City',\n    'America/Guatemala',\n    'America/New_York',\n    'America/Toronto',\n    'America/Bogota',\n    'America/Lima',\n    'America/Havana',\n    'America/Caracas',\n    'America/Halifax',\n    'America/Santiago',\n    'America/La_Paz',\n    'America/St_Johns',\n    'America/Sao_Paulo',\n    'America/Argentina/Buenos_Aires',\n    'Atlantic/South_Georgia',\n    'Atlantic/Azores',\n    'Atlantic/Cape_Verde',\n    'Europe/London',\n    'Africa/Accra',\n    'Atlantic/Reykjavik',\n    'Europe/Paris',\n    'Europe/Berlin',\n    'Europe/Amsterdam',\n    'Europe/Rome',\n    'Europe/Madrid',\n    'Europe/Zurich',\n    'Europe/Stockholm',\n    'Europe/Warsaw',\n    'Africa/Lagos',\n    'Africa/Cairo',\n    'Europe/Athens',\n    'Europe/Bucharest',\n    'Europe/Helsinki',\n    'Europe/Istanbul',\n    'Africa/Johannesburg',\n    'Asia/Jerusalem',\n    'Europe/Moscow',\n    'Asia/Riyadh',\n    'Africa/Nairobi',\n    'Asia/Tehran',\n    'Asia/Dubai',\n    'Asia/Baku',\n    'Asia/Tbilisi',\n    'Asia/Kabul',\n    'Asia/Karachi',\n    'Asia/Tashkent',\n    'Asia/Kolkata',\n    'Asia/Colombo',\n    'Asia/Kathmandu',\n    'Asia/Dhaka',\n    'Asia/Almaty',\n    'Asia/Yangon',\n    'Asia/Bangkok',\n    'Asia/Jakarta',\n    'Asia/Ho_Chi_Minh',\n    'Asia/Shanghai',\n    'Asia/Hong_Kong',\n    'Asia/Singapore',\n    'Asia/Taipei',\n    'Australia/Perth',\n    'Asia/Manila',\n    'Asia/Tokyo',\n    'Asia/Seoul',\n    'Australia/Adelaide',\n    'Australia/Sydney',\n    'Australia/Brisbane',\n    'Australia/Melbourne',\n    'Pacific/Guam',\n    'Pacific/Port_Moresby',\n    'Asia/Vladivostok',\n    'Pacific/Noumea',\n    'Pacific/Auckland',\n    'Pacific/Fiji',\n    'Pacific/Tongatapu',\n    'Pacific/Apia',\n];\n",
      "type": "registry:ui",
      "target": "@ui/orchid/spectrum/molecules/z-timezone-select/z-timezone-select.tsx"
    }
  ],
  "type": "registry:ui"
}