import { useState, useEffect, useRef } from 'react'; import { Input } from '@/components/ui/input'; import { Search, Globe, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; interface TimezoneSelectorProps { value: string; onChange: (timezone: string) => void; className?: string; } // Get all IANA timezones const getAllTimezones = (): string[] => { try { return Intl.supportedValuesOf('timeZone'); } catch { // Fallback for older browsers return [ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Toronto', 'America/Vancouver', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Singapore', 'Australia/Sydney', 'Pacific/Auckland', ]; } }; // Get UTC offset for a timezone const getTimezoneOffset = (timezone: string): string => { try { const now = new Date(); const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'shortOffset', }); const parts = formatter.formatToParts(now); const offsetPart = parts.find((p) => p.type === 'timeZoneName'); return offsetPart?.value || ''; } catch { return ''; } }; // Get current time in a timezone const getCurrentTimeInTimezone = (timezone: string): string => { try { return new Intl.DateTimeFormat('en-US', { timeZone: timezone, hour: 'numeric', minute: '2-digit', hour12: true, }).format(new Date()); } catch { return ''; } }; // Format timezone for display (e.g., "America/New_York" -> "New York") const formatTimezoneLabel = (timezone: string): string => { const parts = timezone.split('/'); const city = parts[parts.length - 1]; return city.replace(/_/g, ' '); }; const ALL_TIMEZONES = getAllTimezones(); export const TimezoneSelector = ({ value, onChange, className, }: TimezoneSelectorProps) => { const [searchQuery, setSearchQuery] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [hoveredTimezone, setHoveredTimezone] = useState(null); const containerRef = useRef(null); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const filteredTimezones = ALL_TIMEZONES.filter((tz) => { const query = searchQuery.toLowerCase(); const tzLower = tz.toLowerCase(); const labelLower = formatTimezoneLabel(tz).toLowerCase(); const offset = getTimezoneOffset(tz).toLowerCase(); return ( tzLower.includes(query) || labelLower.includes(query) || offset.includes(query) ); }); const selectTimezone = (timezone: string) => { onChange(timezone); setSearchQuery(''); setIsDropdownOpen(false); }; const selectedOffset = getTimezoneOffset(value); const selectedLabel = formatTimezoneLabel(value); return (
{isDropdownOpen && (
setSearchQuery(e.target.value)} className="pl-10 h-9 bg-background border-border" autoFocus />
{filteredTimezones.length === 0 ? (
No timezones found
) : ( filteredTimezones.slice(0, 50).map((timezone) => { const isSelected = timezone === value; const offset = getTimezoneOffset(timezone); const label = formatTimezoneLabel(timezone); const isHovered = hoveredTimezone === timezone; return ( ); }) )}
{filteredTimezones.length > 50 && (
Showing 50 of {filteredTimezones.length} results
)}
)}
); };