"use client" import { Clock } from "lucide-react"; import * as React from "react"; import { cn } from "../../../lib/utils"; import { Button } from "../../forms/button"; import { Popover, PopoverContent, PopoverTrigger } from "../../overlay/popover"; // ============================================================================= // Types // ============================================================================= export interface TimePickerProps { /** Selected time value as HH:mm string */ value?: string; /** Default uncontrolled value */ defaultValue?: string; /** Callback when time changes (HH:mm) */ onChange?: (value: string) => void; /** Placeholder text */ placeholder?: string; /** Disable the picker */ disabled?: boolean; /** Use 12-hour format with AM/PM */ use12Hour?: boolean; /** Minimum time (HH:mm) */ min?: string; /** Maximum time (HH:mm) */ max?: string; /** Step in minutes for minute selector @default 1 */ minuteStep?: number; /** Additional class for trigger button */ className?: string; /** Button variant */ variant?: "default" | "outline" | "ghost"; /** Align popover */ align?: "start" | "center" | "end"; /** Form field name */ name?: string; } // ============================================================================= // Utilities // ============================================================================= function parseTime(timeStr: string): { hours: number; minutes: number } | null { const match = timeStr.match(/^(\d{1,2}):(\d{2})$/); if (!match) return null; const hours = parseInt(match[1], 10); const minutes = parseInt(match[2], 10); if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null; return { hours, minutes }; } function formatTime(hours: number, minutes: number): string { return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; } function format12Hour(hours: number, minutes: number): string { const period = hours >= 12 ? "PM" : "AM"; const h = hours % 12 || 12; return `${String(h).padStart(2, "0")}:${String(minutes).padStart(2, "0")} ${period}`; } function isTimeInRange(time: string, min?: string, max?: string): boolean { if (min && time < min) return false; if (max && time > max) return false; return true; } // ============================================================================= // Component // ============================================================================= const TimePicker = React.forwardRef( ( { value: controlledValue, defaultValue, onChange, placeholder = "Select time", disabled = false, use12Hour = false, min, max, minuteStep = 1, className, variant = "outline", align = "start", name, }, ref ) => { const isControlled = controlledValue !== undefined; const [internalValue, setInternalValue] = React.useState(defaultValue ?? ""); const resolvedValue = isControlled ? controlledValue : internalValue; const [open, setOpen] = React.useState(false); const [tempHours, setTempHours] = React.useState(0); const [tempMinutes, setTempMinutes] = React.useState(0); const [tempPeriod, setTempPeriod] = React.useState<"AM" | "PM">("AM"); const parsed = React.useMemo(() => parseTime(resolvedValue), [resolvedValue]); // Sync temp values when opening popover React.useEffect(() => { if (open && parsed) { setTempHours(parsed.hours); setTempMinutes(parsed.minutes); setTempPeriod(parsed.hours >= 12 ? "PM" : "AM"); } else if (open) { setTempHours(12); setTempMinutes(0); setTempPeriod("AM"); } }, [open, parsed]); const updateValue = React.useCallback( (hours: number, minutes: number) => { const formatted = formatTime(hours, minutes); if (!isControlled) { setInternalValue(formatted); } onChange?.(formatted); }, [isControlled, onChange] ); const handleApply = React.useCallback(() => { let hours = tempHours; if (use12Hour) { if (tempPeriod === "PM" && hours !== 12) hours += 12; if (tempPeriod === "AM" && hours === 12) hours = 0; } const formatted = formatTime(hours, tempMinutes); if (isTimeInRange(formatted, min, max)) { updateValue(hours, tempMinutes); setOpen(false); } }, [tempHours, tempMinutes, tempPeriod, use12Hour, min, max, updateValue]); const displayValue = React.useMemo(() => { if (!parsed) return placeholder; if (use12Hour) { return format12Hour(parsed.hours, parsed.minutes); } return formatTime(parsed.hours, parsed.minutes); }, [parsed, use12Hour, placeholder]); // Generate hour options const hourOptions = React.useMemo(() => { if (use12Hour) { return Array.from({ length: 12 }, (_, i) => i + 1); } return Array.from({ length: 24 }, (_, i) => i); }, [use12Hour]); // Generate minute options const minuteOptions = React.useMemo(() => { const steps = Math.floor(60 / minuteStep); return Array.from({ length: steps }, (_, i) => i * minuteStep); }, [minuteStep]); const isFormControl = React.useRef(false); const rootRef = React.useRef(null); React.useImperativeHandle(ref, () => rootRef.current as unknown as HTMLButtonElement); React.useEffect(() => { if (rootRef.current) { isFormControl.current = !!rootRef.current.closest("form"); } }, []); return ( <>
{/* Hours */}
Hour
{hourOptions.map((h) => { const selected = use12Hour ? (tempHours % 12 || 12) === h : tempHours === h; return ( ); })}
: {/* Minutes */}
Min
{minuteOptions.map((m) => { const selected = tempMinutes === m; return ( ); })}
{/* AM/PM */} {use12Hour && (
 
{(["AM", "PM"] as const).map((p) => ( ))}
)}
{isFormControl.current && name && ( )} ); } ); TimePicker.displayName = "TimePicker"; export { TimePicker };