diff --git a/docs/public/r/styles/default/time-picker.json b/docs/public/r/styles/default/time-picker.json index 6a41a635..8734685a 100644 --- a/docs/public/r/styles/default/time-picker.json +++ b/docs/public/r/styles/default/time-picker.json @@ -10,7 +10,7 @@ "files": [ { "path": "ui/time-picker.tsx", - "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Clock } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/default/components/visually-hidden-input\";\n\nconst ROOT_NAME = \"TimePicker\";\nconst LABEL_NAME = \"TimePickerLabel\";\nconst INPUT_GROUP_NAME = \"TimePickerInputGroup\";\nconst INPUT_NAME = \"TimePickerInput\";\nconst TRIGGER_NAME = \"TimePickerTrigger\";\nconst COLUMN_NAME = \"TimePickerColumn\";\nconst COLUMN_ITEM_NAME = \"TimePickerColumnItem\";\nconst HOUR_NAME = \"TimePickerHour\";\nconst MINUTE_NAME = \"TimePickerMinute\";\nconst SECOND_NAME = \"TimePickerSecond\";\nconst PERIOD_NAME = \"TimePickerPeriod\";\nconst CLEAR_NAME = \"TimePickerClear\";\n\nconst DEFAULT_STEP = 1;\nconst DEFAULT_SEGMENT_PLACEHOLDER = \"--\";\nconst DEFAULT_LOCALE = undefined;\nconst PERIODS = [\"AM\", \"PM\"] as const;\n\ntype Segment = \"hour\" | \"minute\" | \"second\" | \"period\";\ntype SegmentFormat = \"numeric\" | \"2-digit\";\ntype Period = (typeof PERIODS)[number];\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ninterface ButtonProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef;\ntype InputElement = React.ComponentRef;\ntype ColumnElement = React.ComponentRef;\ntype ColumnItemElement = React.ComponentRef;\n\ninterface TimeValue {\n hour?: number;\n minute?: number;\n second?: number;\n period?: Period;\n}\n\ninterface ItemData {\n value: number | string;\n ref: React.RefObject;\n selected: boolean;\n}\n\ninterface ColumnData {\n id: string;\n ref: React.RefObject;\n getSelectedItemRef: () => React.RefObject | null;\n getItems: () => ItemData[];\n}\n\nconst useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nfunction useAsRef(props: T) {\n const ref = React.useRef(props);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = props;\n });\n\n return ref;\n}\n\nfunction useLazyRef(fn: () => T) {\n const ref = React.useRef(null);\n\n if (ref.current === null) {\n ref.current = fn();\n }\n\n return ref as React.RefObject;\n}\n\nfunction focusFirst(\n candidates: React.RefObject[],\n preventScroll = false,\n) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidateRef of candidates) {\n const candidate = candidateRef.current;\n if (!candidate) continue;\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus({ preventScroll });\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\nfunction sortNodes }>(\n items: T[],\n): T[] {\n return items.sort((a, b) => {\n const elementA = a.ref.current;\n const elementB = b.ref.current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n return -1;\n }\n if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n return 1;\n }\n return 0;\n });\n}\n\nfunction getIs12Hour(locale?: string): boolean {\n const testDate = new Date(2000, 0, 1, 13, 0, 0);\n const formatted = new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n }).format(testDate);\n\n return /am|pm/i.test(formatted) || !formatted.includes(\"13\");\n}\n\nfunction parseTimeString(timeString: string | undefined): TimeValue | null {\n if (!timeString) return null;\n\n const parts = timeString.split(\":\");\n if (parts.length < 2) return null;\n\n const result: TimeValue = {};\n\n if (parts[0] && parts[0] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const hour = Number.parseInt(parts[0], 10);\n if (!Number.isNaN(hour) && hour >= 0 && hour <= 23) {\n result.hour = hour;\n }\n }\n\n if (parts[1] && parts[1] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const minute = Number.parseInt(parts[1], 10);\n if (!Number.isNaN(minute) && minute >= 0 && minute <= 59) {\n result.minute = minute;\n }\n }\n\n if (parts[2] && parts[2] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const second = Number.parseInt(parts[2], 10);\n if (!Number.isNaN(second) && second >= 0 && second <= 59) {\n result.second = second;\n }\n }\n if (\n result.hour === undefined &&\n result.minute === undefined &&\n result.second === undefined\n ) {\n return null;\n }\n\n return result;\n}\n\nfunction formatTimeValue(value: TimeValue, showSeconds: boolean): string {\n const hourStr =\n value.hour !== undefined\n ? value.hour.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n const minuteStr =\n value.minute !== undefined\n ? value.minute.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n const secondStr =\n value.second !== undefined\n ? value.second.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n\n if (showSeconds) {\n return `${hourStr}:${minuteStr}:${secondStr}`;\n }\n return `${hourStr}:${minuteStr}`;\n}\n\nfunction to12Hour(hour24: number): { hour: number; period: Period } {\n const period: Period = hour24 >= 12 ? \"PM\" : \"AM\";\n const hour = hour24 % 12 || 12;\n return { hour, period };\n}\n\nfunction to24Hour(hour12: number, period: Period): number {\n if (hour12 === 12) {\n return period === \"PM\" ? 12 : 0;\n }\n return period === \"PM\" ? hour12 + 12 : hour12;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max);\n}\n\ninterface StoreState {\n value: string;\n open: boolean;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: (key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = useStoreContext(\"useStore\");\n\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ntype SegmentPlaceholder =\n | string\n | {\n hour?: string;\n minute?: string;\n second?: string;\n period?: string;\n };\n\ninterface TimePickerContextValue {\n id: string;\n inputGroupId: string;\n labelId: string;\n triggerId: string;\n disabled: boolean;\n readOnly: boolean;\n required: boolean;\n invalid: boolean;\n showSeconds: boolean;\n is12Hour: boolean;\n minuteStep: number;\n secondStep: number;\n hourStep: number;\n segmentPlaceholder: {\n hour: string;\n minute: string;\n second: string;\n period: string;\n };\n min?: string;\n max?: string;\n}\n\nconst TimePickerContext = React.createContext(\n null,\n);\n\nfunction useTimePickerContext(consumerName: string) {\n const context = React.useContext(TimePickerContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimePickerRootProps extends DivProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n min?: string;\n max?: string;\n hourStep?: number;\n minuteStep?: number;\n secondStep?: number;\n segmentPlaceholder?: SegmentPlaceholder;\n locale?: string;\n name?: string;\n disabled?: boolean;\n invalid?: boolean;\n readOnly?: boolean;\n required?: boolean;\n showSeconds?: boolean;\n}\n\nfunction TimePickerRoot(props: TimePickerRootProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n open,\n defaultOpen,\n onOpenChange,\n ...rootProps\n } = props;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n value: value ?? defaultValue ?? \"\",\n open: open ?? defaultOpen ?? false,\n }));\n const propsRef = useAsRef({ onValueChange, onOpenChange });\n\n const store: Store = React.useMemo(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n if (key === \"value\" && typeof value === \"string\") {\n stateRef.current.value = value;\n propsRef.current.onValueChange?.(value);\n } else if (key === \"open\" && typeof value === \"boolean\") {\n stateRef.current.open = value;\n propsRef.current.onOpenChange?.(value);\n } else {\n stateRef.current[key] = value;\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n return (\n \n \n \n );\n}\n\ninterface TimePickerRootImplProps\n extends Omit<\n TimePickerRootProps,\n \"defaultValue\" | \"defaultOpen\" | \"onValueChange\" | \"onOpenChange\"\n > {}\n\nfunction TimePickerRootImpl(props: TimePickerRootImplProps) {\n const {\n value,\n open: openProp,\n min,\n max,\n hourStep = DEFAULT_STEP,\n minuteStep = DEFAULT_STEP,\n secondStep = DEFAULT_STEP,\n segmentPlaceholder = DEFAULT_SEGMENT_PLACEHOLDER,\n locale = DEFAULT_LOCALE,\n name,\n asChild,\n disabled = false,\n invalid = false,\n readOnly = false,\n required = false,\n showSeconds = false,\n className,\n children,\n id,\n ref,\n ...rootProps\n } = props;\n\n const store = useStoreContext(\"TimePickerRootImpl\");\n\n useIsomorphicLayoutEffect(() => {\n if (value !== undefined) {\n store.setState(\"value\", value);\n }\n }, [value]);\n\n useIsomorphicLayoutEffect(() => {\n if (openProp !== undefined) {\n store.setState(\"open\", openProp);\n }\n }, [openProp]);\n\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n const inputGroupId = React.useId();\n const labelId = React.useId();\n const triggerId = React.useId();\n\n const [formTrigger, setFormTrigger] = React.useState(\n null,\n );\n const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n\n const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n const open = useStore((state) => state.open);\n\n const onPopoverOpenChange = React.useCallback(\n (newOpen: boolean) => store.setState(\"open\", newOpen),\n [store],\n );\n\n const is12Hour = React.useMemo(() => getIs12Hour(locale), [locale]);\n\n const normalizedPlaceholder = React.useMemo(() => {\n if (typeof segmentPlaceholder === \"string\") {\n return {\n hour: segmentPlaceholder,\n minute: segmentPlaceholder,\n second: segmentPlaceholder,\n period: segmentPlaceholder,\n };\n }\n return {\n hour: segmentPlaceholder.hour ?? DEFAULT_SEGMENT_PLACEHOLDER,\n minute: segmentPlaceholder.minute ?? DEFAULT_SEGMENT_PLACEHOLDER,\n second: segmentPlaceholder.second ?? DEFAULT_SEGMENT_PLACEHOLDER,\n period: segmentPlaceholder.period ?? DEFAULT_SEGMENT_PLACEHOLDER,\n };\n }, [segmentPlaceholder]);\n\n const rootContext = React.useMemo(\n () => ({\n id: rootId,\n inputGroupId,\n labelId,\n triggerId,\n disabled,\n readOnly,\n required,\n invalid,\n showSeconds,\n is12Hour,\n minuteStep,\n secondStep,\n hourStep,\n segmentPlaceholder: normalizedPlaceholder,\n min,\n max,\n }),\n [\n rootId,\n inputGroupId,\n labelId,\n triggerId,\n disabled,\n readOnly,\n required,\n invalid,\n showSeconds,\n is12Hour,\n minuteStep,\n secondStep,\n hourStep,\n normalizedPlaceholder,\n min,\n max,\n ],\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <>\n \n \n \n {children}\n \n \n \n {isFormControl && (\n \n )}\n \n );\n}\n\ninterface TimePickerLabelProps extends React.ComponentProps<\"label\"> {\n asChild?: boolean;\n}\n\nfunction TimePickerLabel(props: TimePickerLabelProps) {\n const { asChild, className, ...labelProps } = props;\n\n const { labelId } = useTimePickerContext(LABEL_NAME);\n\n const LabelPrimitive = asChild ? Slot : \"label\";\n\n return (\n \n );\n}\n\ninterface TimePickerInputGroupContextValue {\n onInputRegister: (\n segment: Segment,\n ref: React.RefObject,\n ) => void;\n onInputUnregister: (segment: Segment) => void;\n getNextInput: (\n currentSegment: Segment,\n ) => React.RefObject | null;\n}\n\nconst TimePickerInputGroupContext =\n React.createContext(null);\n\nfunction useTimePickerInputGroupContext(consumerName: string) {\n const context = React.useContext(TimePickerInputGroupContext);\n if (!context) {\n throw new Error(\n `\\`${consumerName}\\` must be used within \\`${INPUT_GROUP_NAME}\\``,\n );\n }\n return context;\n}\n\nfunction TimePickerInputGroup(props: DivProps) {\n const { asChild, className, style, ...inputGroupProps } = props;\n\n const { inputGroupId, labelId, disabled, invalid, segmentPlaceholder } =\n useTimePickerContext(INPUT_GROUP_NAME);\n\n const inputRefsMap = React.useRef<\n Map>\n >(new Map());\n\n const onInputRegister = React.useCallback(\n (segment: Segment, ref: React.RefObject) => {\n inputRefsMap.current.set(segment, ref);\n },\n [],\n );\n\n const onInputUnregister = React.useCallback((segment: Segment) => {\n inputRefsMap.current.delete(segment);\n }, []);\n\n const getNextInput = React.useCallback(\n (currentSegment: Segment): React.RefObject | null => {\n const segmentOrder: Segment[] = [\"hour\", \"minute\", \"second\", \"period\"];\n const currentIndex = segmentOrder.indexOf(currentSegment);\n\n if (currentIndex === -1 || currentIndex === segmentOrder.length - 1) {\n return null;\n }\n\n for (let i = currentIndex + 1; i < segmentOrder.length; i++) {\n const nextSegment = segmentOrder[i];\n if (nextSegment) {\n const nextRef = inputRefsMap.current.get(nextSegment);\n if (nextRef?.current) {\n return nextRef;\n }\n }\n }\n\n return null;\n },\n [],\n );\n\n const inputGroupContextValue =\n React.useMemo(\n () => ({\n onInputRegister,\n onInputUnregister,\n getNextInput,\n }),\n [onInputRegister, onInputUnregister, getNextInput],\n );\n\n const InputGroupPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n \n \n );\n}\n\ninterface TimePickerInputProps\n extends Omit, \"type\" | \"value\"> {\n segment?: Segment;\n}\n\nfunction TimePickerInput(props: TimePickerInputProps) {\n const {\n segment,\n disabled: disabledProp,\n readOnly: readOnlyProp,\n className,\n style,\n ref,\n onBlur: onBlurProp,\n onChange: onChangeProp,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n ...inputProps\n } = props;\n\n const { is12Hour, showSeconds, disabled, readOnly, segmentPlaceholder } =\n useTimePickerContext(INPUT_NAME);\n const store = useStoreContext(INPUT_NAME);\n const inputGroupContext = useTimePickerInputGroupContext(INPUT_NAME);\n\n const isDisabled = disabledProp || disabled;\n const isReadOnly = readOnlyProp || readOnly;\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const inputRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, inputRef);\n\n useIsomorphicLayoutEffect(() => {\n if (segment) {\n inputGroupContext.onInputRegister(segment as Segment, inputRef);\n return () => inputGroupContext.onInputUnregister(segment as Segment);\n }\n }, [inputGroupContext, segment]);\n\n const getSegmentValue = React.useCallback(() => {\n if (!timeValue) {\n if (!segment) return \"\";\n return segmentPlaceholder[segment];\n }\n switch (segment) {\n case \"hour\": {\n if (timeValue.hour === undefined) return segmentPlaceholder.hour;\n if (is12Hour) {\n return to12Hour(timeValue.hour).hour.toString().padStart(2, \"0\");\n }\n return timeValue.hour.toString().padStart(2, \"0\");\n }\n case \"minute\":\n if (timeValue.minute === undefined) return segmentPlaceholder.minute;\n return timeValue.minute.toString().padStart(2, \"0\");\n case \"second\":\n if (timeValue.second === undefined) return segmentPlaceholder.second;\n return timeValue.second.toString().padStart(2, \"0\");\n case \"period\":\n if (!timeValue || timeValue.hour === undefined)\n return segmentPlaceholder.period;\n return to12Hour(timeValue.hour).period;\n default:\n return \"\";\n }\n }, [timeValue, segment, is12Hour, segmentPlaceholder]);\n\n const [editValue, setEditValue] = React.useState(getSegmentValue());\n const [isEditing, setIsEditing] = React.useState(false);\n const [pendingDigit, setPendingDigit] = React.useState(null);\n\n React.useEffect(() => {\n if (!isEditing) {\n setEditValue(getSegmentValue());\n setPendingDigit(null);\n }\n }, [getSegmentValue, isEditing]);\n\n const updateTimeValue = React.useCallback(\n (newSegmentValue: string | undefined, shouldCreateIfEmpty = false) => {\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (!newSegmentValue || newSegmentValue === placeholder) return;\n if (!timeValue && !shouldCreateIfEmpty) return;\n\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime };\n\n switch (segment) {\n case \"hour\": {\n const displayHour = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(displayHour)) {\n if (is12Hour) {\n const clampedHour = clamp(displayHour, 1, 12);\n const currentPeriod = timeValue?.period || \"AM\";\n newTime.hour = to24Hour(clampedHour, currentPeriod);\n if (timeValue?.period !== undefined) {\n newTime.period = timeValue.period;\n }\n } else {\n newTime.hour = clamp(displayHour, 0, 23);\n }\n }\n break;\n }\n case \"minute\": {\n const minute = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(minute)) {\n newTime.minute = clamp(minute, 0, 59);\n }\n break;\n }\n case \"second\": {\n const second = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(second)) {\n newTime.second = clamp(second, 0, 59);\n }\n break;\n }\n case \"period\": {\n if (newSegmentValue === \"AM\" || newSegmentValue === \"PM\") {\n newTime.period = newSegmentValue;\n if (timeValue && timeValue.hour !== undefined) {\n const currentDisplay = to12Hour(timeValue.hour);\n newTime.hour = to24Hour(currentDisplay.hour, newSegmentValue);\n }\n }\n break;\n }\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, segment, is12Hour, showSeconds, store, segmentPlaceholder],\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent) => {\n onBlurProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsEditing(false);\n\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue && editValue !== placeholder && editValue.length > 0) {\n let valueToUpdate = editValue;\n\n if (segment !== \"period\") {\n if (editValue.length === 2) {\n valueToUpdate = editValue;\n } else if (editValue.length === 1) {\n const numValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(numValue)) {\n valueToUpdate = numValue.toString().padStart(2, \"0\");\n }\n }\n }\n\n updateTimeValue(valueToUpdate, true);\n\n queueMicrotask(() => {\n const currentTimeValue = parseTimeString(store.getState().value);\n if (currentTimeValue) {\n const now = new Date();\n const newTime = { ...currentTimeValue };\n let needsUpdate = false;\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n needsUpdate = true;\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n needsUpdate = true;\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n needsUpdate = true;\n }\n\n if (needsUpdate) {\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n }\n }\n });\n }\n\n setEditValue(getSegmentValue());\n setPendingDigit(null);\n },\n [\n onBlurProp,\n editValue,\n updateTimeValue,\n getSegmentValue,\n segment,\n segmentPlaceholder,\n showSeconds,\n store,\n ],\n );\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n onChangeProp?.(event);\n if (event.defaultPrevented) return;\n\n let newValue = event.target.value;\n\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (\n editValue === placeholder &&\n newValue.length > 0 &&\n newValue !== placeholder\n ) {\n newValue = newValue.replace(new RegExp(`^${placeholder}`), \"\");\n }\n\n if (segment === \"period\") {\n const firstChar = newValue.charAt(0).toUpperCase();\n let newPeriod: Period | null = null;\n\n if (firstChar === \"A\" || firstChar === \"1\") {\n newPeriod = \"AM\";\n } else if (firstChar === \"P\" || firstChar === \"2\") {\n newPeriod = \"PM\";\n }\n\n if (newPeriod) {\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n return;\n }\n\n if (segment === \"hour\" || segment === \"minute\" || segment === \"second\") {\n newValue = newValue.replace(/\\D/g, \"\");\n }\n\n if (newValue.length > 2) {\n newValue = newValue.slice(0, 2);\n }\n if (segment === \"hour\" || segment === \"minute\" || segment === \"second\") {\n const numValue = Number.parseInt(newValue, 10);\n\n if (!Number.isNaN(numValue) && newValue.length > 0) {\n if (pendingDigit !== null && newValue.length === 1) {\n const twoDigitValue = pendingDigit + newValue;\n const combinedNum = Number.parseInt(twoDigitValue, 10);\n\n if (!Number.isNaN(combinedNum)) {\n const paddedValue = combinedNum.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n return;\n }\n }\n\n const maxFirstDigit = segment === \"hour\" ? (is12Hour ? 1 : 2) : 5;\n\n const firstDigit = Number.parseInt(newValue[0] ?? \"0\", 10);\n const shouldAutoAdvance = firstDigit > maxFirstDigit;\n\n if (newValue.length === 1) {\n if (shouldAutoAdvance) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n } else {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n setPendingDigit(newValue);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n } else if (newValue.length === 2) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n }\n } else if (newValue.length === 0) {\n setEditValue(\"\");\n setPendingDigit(null);\n }\n }\n },\n [\n segment,\n updateTimeValue,\n onChangeProp,\n editValue,\n is12Hour,\n inputGroupContext,\n pendingDigit,\n segmentPlaceholder,\n ],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n event.currentTarget.select();\n },\n [onClickProp],\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsEditing(true);\n setPendingDigit(null);\n queueMicrotask(() => event.target.select());\n },\n [onFocusProp],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n onKeyDownProp?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n event.preventDefault();\n\n const goToPrevious = event.key === \"ArrowLeft\";\n const inputGroup = inputRef.current?.closest(\n '[data-slot=\"time-picker-input-group\"]',\n );\n\n if (inputGroup && inputRef.current) {\n const allInputs = Array.from(\n inputGroup.querySelectorAll('input[type=\"text\"]'),\n ) as HTMLInputElement[];\n const currentIdx = allInputs.indexOf(inputRef.current);\n\n if (currentIdx !== -1) {\n const targetIdx = goToPrevious\n ? Math.max(0, currentIdx - 1)\n : Math.min(allInputs.length - 1, currentIdx + 1);\n\n const targetInput = allInputs[targetIdx];\n if (targetInput && targetInput !== inputRef.current) {\n targetInput.focus();\n targetInput.select();\n }\n }\n }\n return;\n }\n\n if (event.key === \"Backspace\" || event.key === \"Delete\") {\n const input = inputRef.current;\n if (\n input &&\n input.selectionStart === 0 &&\n input.selectionEnd === input.value.length\n ) {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n setEditValue(placeholder);\n setPendingDigit(null);\n\n if (timeValue) {\n const newTime = { ...timeValue };\n switch (segment) {\n case \"hour\":\n delete newTime.hour;\n break;\n case \"minute\":\n delete newTime.minute;\n break;\n case \"second\":\n delete newTime.second;\n break;\n case \"period\":\n delete newTime.period;\n break;\n }\n\n if (\n newTime.hour !== undefined ||\n newTime.minute !== undefined ||\n newTime.second !== undefined ||\n newTime.period !== undefined\n ) {\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n } else {\n store.setState(\"value\", \"\");\n }\n } else {\n store.setState(\"value\", \"\");\n }\n\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n return;\n }\n }\n\n if (segment === \"period\") {\n const key = event.key.toLowerCase();\n if (key === \"a\" || key === \"p\" || key === \"1\" || key === \"2\") {\n event.preventDefault();\n let newPeriod: Period;\n if (key === \"a\" || key === \"1\") {\n newPeriod = \"AM\";\n } else {\n newPeriod = \"PM\";\n }\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n } else if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n event.preventDefault();\n const placeholder = segmentPlaceholder.period;\n const currentPeriod =\n editValue === placeholder || editValue === \"\" ? \"AM\" : editValue;\n const newPeriod =\n currentPeriod === \"AM\" || currentPeriod === \"A\" ? \"PM\" : \"AM\";\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n return;\n }\n\n if (event.key === \"Tab\") {\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue && editValue.length > 0 && editValue !== placeholder) {\n if (editValue.length === 2) {\n updateTimeValue(editValue, true);\n } else if (editValue.length === 1) {\n const numValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(numValue)) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n updateTimeValue(paddedValue, true);\n }\n }\n }\n return;\n }\n\n if (event.key === \"Enter\") {\n event.preventDefault();\n inputRef.current?.blur();\n }\n\n if (event.key === \"Escape\") {\n event.preventDefault();\n setEditValue(getSegmentValue());\n inputRef.current?.blur();\n }\n\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue === placeholder || editValue === \"\") {\n const defaultValue = segment === \"hour\" ? (is12Hour ? 12 : 0) : 0;\n const formattedValue = defaultValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n return;\n }\n const currentValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(currentValue)) {\n let newValue: number;\n switch (segment) {\n case \"hour\":\n if (is12Hour) {\n newValue = currentValue === 12 ? 1 : currentValue + 1;\n } else {\n newValue = currentValue === 23 ? 0 : currentValue + 1;\n }\n break;\n case \"minute\":\n case \"second\":\n newValue = currentValue === 59 ? 0 : currentValue + 1;\n break;\n default:\n return;\n }\n const formattedValue = newValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n }\n }\n\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue === placeholder || editValue === \"\") {\n const defaultValue = segment === \"hour\" ? (is12Hour ? 12 : 23) : 59;\n const formattedValue = defaultValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n return;\n }\n const currentValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(currentValue)) {\n let newValue: number;\n switch (segment) {\n case \"hour\":\n if (is12Hour) {\n newValue = currentValue === 1 ? 12 : currentValue - 1;\n } else {\n newValue = currentValue === 0 ? 23 : currentValue - 1;\n }\n break;\n case \"minute\":\n case \"second\":\n newValue = currentValue === 0 ? 59 : currentValue - 1;\n break;\n default:\n return;\n }\n const formattedValue = newValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n }\n }\n },\n [\n onKeyDownProp,\n editValue,\n segment,\n is12Hour,\n getSegmentValue,\n updateTimeValue,\n showSeconds,\n timeValue,\n store,\n segmentPlaceholder,\n ],\n );\n\n const displayValue = isEditing ? editValue : getSegmentValue();\n\n const segmentWidth = segment\n ? `var(--time-picker-${segment}-input-width)`\n : \"2ch\";\n\n return (\n \n );\n}\n\nfunction TimePickerTrigger(props: ButtonProps) {\n const {\n className,\n children,\n disabled: disabledProp,\n ...triggerProps\n } = props;\n\n const { triggerId, disabled } = useTimePickerContext(TRIGGER_NAME);\n\n const isDisabled = disabledProp || disabled;\n\n return (\n svg:not([class*='size-'])]:size-4\",\n className,\n )}\n >\n {children ?? }\n \n );\n}\n\ninterface TimePickerGroupContextValue {\n getColumns: () => ColumnData[];\n onColumnRegister: (column: ColumnData) => void;\n onColumnUnregister: (id: string) => void;\n}\n\nconst TimePickerGroupContext =\n React.createContext(null);\n\nfunction useTimePickerGroupContext(consumerName: string) {\n const context = React.useContext(TimePickerGroupContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimePickerContentProps\n extends DivProps,\n React.ComponentProps {}\n\nfunction TimePickerContent(props: TimePickerContentProps) {\n const {\n side = \"bottom\",\n align = \"start\",\n sideOffset = 4,\n className,\n onOpenAutoFocus: onOpenAutoFocusProp,\n children,\n ...contentProps\n } = props;\n\n const columnsRef = React.useRef>>(\n new Map(),\n );\n\n const onColumnRegister = React.useCallback((column: ColumnData) => {\n columnsRef.current.set(column.id, column);\n }, []);\n\n const onColumnUnregister = React.useCallback((id: string) => {\n columnsRef.current.delete(id);\n }, []);\n\n const getColumns = React.useCallback(() => {\n const columns = Array.from(columnsRef.current.entries())\n .map(([id, { ref, getSelectedItemRef, getItems }]) => ({\n id,\n ref,\n getSelectedItemRef,\n getItems,\n }))\n .filter((c) => c.ref.current !== null);\n return sortNodes(columns);\n }, []);\n\n const groupContextValue = React.useMemo(\n () => ({\n getColumns,\n onColumnRegister,\n onColumnUnregister,\n }),\n [getColumns, onColumnRegister, onColumnUnregister],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback(\n (event) => {\n onOpenAutoFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n const columns = getColumns();\n const firstColumn = columns[0];\n\n if (!firstColumn) return;\n\n const items = firstColumn.getItems();\n const selectedItem = items.find((item) => item.selected);\n\n const candidateRefs = selectedItem\n ? [selectedItem.ref, ...items.map((item) => item.ref)]\n : items.map((item) => item.ref);\n\n focusFirst(candidateRefs, false);\n },\n [onOpenAutoFocusProp, getColumns],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface TimePickerColumnContextValue {\n getItems: () => ItemData[];\n onItemRegister: (\n value: number | string,\n ref: React.RefObject,\n selected: boolean,\n ) => void;\n onItemUnregister: (value: number | string) => void;\n}\n\nconst TimePickerColumnContext =\n React.createContext(null);\n\nfunction useTimePickerColumnContext(consumerName: string) {\n const context = React.useContext(TimePickerColumnContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within a column`);\n }\n return context;\n}\n\ninterface TimePickerColumnProps extends DivProps {}\n\nfunction TimePickerColumn(props: TimePickerColumnProps) {\n const { children, className, ref, ...columnProps } = props;\n\n const columnId = React.useId();\n const columnRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, columnRef);\n\n const itemsRef = React.useRef<\n Map<\n number | string,\n {\n ref: React.RefObject;\n selected: boolean;\n }\n >\n >(new Map());\n\n const groupContext = useTimePickerGroupContext(COLUMN_NAME);\n\n const onItemRegister = React.useCallback(\n (\n value: number | string,\n ref: React.RefObject,\n selected: boolean,\n ) => {\n itemsRef.current.set(value, { ref, selected });\n },\n [],\n );\n\n const onItemUnregister = React.useCallback((value: number | string) => {\n itemsRef.current.delete(value);\n }, []);\n\n const getItems = React.useCallback(() => {\n const items = Array.from(itemsRef.current.entries())\n .map(([value, { ref, selected }]) => ({\n value,\n ref,\n selected,\n }))\n .filter((item) => item.ref.current);\n return sortNodes(items);\n }, []);\n\n const getSelectedItemRef = React.useCallback(() => {\n const items = getItems();\n return items.find((item) => item.selected)?.ref ?? null;\n }, [getItems]);\n\n useIsomorphicLayoutEffect(() => {\n groupContext.onColumnRegister({\n id: columnId,\n ref: columnRef,\n getSelectedItemRef,\n getItems,\n });\n return () => groupContext.onColumnUnregister(columnId);\n }, [groupContext, columnId, getSelectedItemRef, getItems]);\n\n const columnContextValue = React.useMemo(\n () => ({\n getItems,\n onItemRegister,\n onItemUnregister,\n }),\n [getItems, onItemRegister, onItemUnregister],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface TimePickerColumnItemProps extends ButtonProps {\n value: number | string;\n selected?: boolean;\n format?: SegmentFormat;\n}\n\nfunction TimePickerColumnItem(props: TimePickerColumnItemProps) {\n const {\n value,\n selected = false,\n format = \"numeric\",\n className,\n ref,\n ...itemProps\n } = props;\n\n const itemRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, itemRef);\n const columnContext = useTimePickerColumnContext(COLUMN_ITEM_NAME);\n const groupContext = useTimePickerGroupContext(COLUMN_ITEM_NAME);\n\n useIsomorphicLayoutEffect(() => {\n columnContext.onItemRegister(value, itemRef, selected);\n return () => columnContext.onItemUnregister(value);\n }, [columnContext, value, selected]);\n\n useIsomorphicLayoutEffect(() => {\n if (selected && itemRef.current) {\n itemRef.current.scrollIntoView({ block: \"nearest\" });\n }\n }, [selected]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n itemProps.onClick?.(event);\n if (event.defaultPrevented) return;\n\n itemRef.current?.focus();\n },\n [itemProps.onClick],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n itemProps.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n event.preventDefault();\n const items = columnContext.getItems().sort((a, b) => {\n if (typeof a.value === \"number\" && typeof b.value === \"number\") {\n return a.value - b.value;\n }\n return 0;\n });\n const currentIndex = items.findIndex((item) => item.value === value);\n\n let nextIndex: number;\n if (event.key === \"ArrowUp\") {\n nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;\n } else {\n nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;\n }\n\n const nextItem = items[nextIndex];\n nextItem?.ref.current?.focus();\n nextItem?.ref.current?.click();\n } else if (\n (event.key === \"Tab\" ||\n event.key === \"ArrowLeft\" ||\n event.key === \"ArrowRight\") &&\n groupContext\n ) {\n event.preventDefault();\n\n queueMicrotask(() => {\n const columns = groupContext.getColumns();\n\n if (columns.length === 0) return;\n\n const currentColumnIndex = columns.findIndex(\n (c) => c.ref.current?.contains(itemRef.current) ?? false,\n );\n\n if (currentColumnIndex === -1) return;\n\n const goToPrevious =\n event.key === \"ArrowLeft\" ||\n (event.key === \"Tab\" && event.shiftKey);\n\n const nextColumnIndex = goToPrevious\n ? currentColumnIndex > 0\n ? currentColumnIndex - 1\n : columns.length - 1\n : currentColumnIndex < columns.length - 1\n ? currentColumnIndex + 1\n : 0;\n\n const nextColumn = columns[nextColumnIndex];\n if (nextColumn?.ref.current) {\n const items = nextColumn.getItems();\n const selectedItem = items.find((item) => item.selected);\n\n const candidateRefs = selectedItem\n ? [selectedItem.ref, ...items.map((item) => item.ref)]\n : items.map((item) => item.ref);\n\n focusFirst(candidateRefs, false);\n }\n });\n }\n },\n [itemProps.onKeyDown, columnContext, groupContext, value],\n );\n\n const formattedValue =\n typeof value === \"number\" && format === \"2-digit\"\n ? value.toString().padStart(2, \"0\")\n : value.toString();\n\n return (\n \n {formattedValue}\n \n );\n}\n\ninterface TimePickerHourProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerHour(props: TimePickerHourProps) {\n const { asChild, format = \"numeric\", className, ...hourProps } = props;\n\n const { is12Hour, hourStep, showSeconds } = useTimePickerContext(HOUR_NAME);\n const store = useStoreContext(HOUR_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const hours = Array.from(\n {\n length: is12Hour ? Math.ceil(12 / hourStep) : Math.ceil(24 / hourStep),\n },\n (_, i) => {\n if (is12Hour) {\n const hour = (i * hourStep) % 12;\n return hour === 0 ? 12 : hour;\n }\n return i * hourStep;\n },\n );\n\n const onHourSelect = React.useCallback(\n (displayHour: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n\n let hour24 = displayHour;\n if (is12Hour) {\n const currentPeriod = timeValue?.period || \"AM\";\n hour24 = to24Hour(displayHour, currentPeriod);\n }\n\n const newTime = { ...currentTime, hour: hour24 };\n if (timeValue && timeValue.period !== undefined) {\n newTime.period = timeValue.period;\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, is12Hour, store],\n );\n\n const now = new Date();\n const referenceHour = timeValue?.hour ?? now.getHours();\n const displayHour = is12Hour ? to12Hour(referenceHour).hour : referenceHour;\n\n const HourPrimitive = asChild ? Slot : TimePickerColumn;\n\n return (\n \n {hours.map((hour) => (\n onHourSelect(hour)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerMinuteProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerMinute(props: TimePickerMinuteProps) {\n const { asChild, format = \"2-digit\", className, ...minuteProps } = props;\n\n const { minuteStep, showSeconds } = useTimePickerContext(MINUTE_NAME);\n const store = useStoreContext(MINUTE_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const minutes = Array.from(\n { length: Math.ceil(60 / minuteStep) },\n (_, i) => i * minuteStep,\n );\n\n const onMinuteSelect = React.useCallback(\n (minute: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime, minute };\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, store],\n );\n\n const MinutePrimitive = asChild ? Slot : TimePickerColumn;\n\n const now = new Date();\n const referenceMinute = timeValue?.minute ?? now.getMinutes();\n\n return (\n \n {minutes.map((minute) => (\n onMinuteSelect(minute)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerSecondProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerSecond(props: TimePickerSecondProps) {\n const { asChild, format = \"2-digit\", className, ...secondProps } = props;\n\n const { secondStep } = useTimePickerContext(SECOND_NAME);\n const store = useStoreContext(SECOND_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const seconds = Array.from(\n { length: Math.ceil(60 / secondStep) },\n (_, i) => i * secondStep,\n );\n\n const onSecondSelect = React.useCallback(\n (second: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime, second };\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n const newValue = formatTimeValue(newTime, true);\n store.setState(\"value\", newValue);\n },\n [timeValue, store],\n );\n\n const SecondPrimitive = asChild ? Slot : TimePickerColumn;\n\n const now = new Date();\n const referenceSecond = timeValue?.second ?? now.getSeconds();\n\n return (\n \n {seconds.map((second) => (\n onSecondSelect(second)}\n />\n ))}\n \n );\n}\n\nfunction TimePickerPeriod(props: DivProps) {\n const { asChild, className, ...periodProps } = props;\n\n const { is12Hour, showSeconds } = useTimePickerContext(PERIOD_NAME);\n const store = useStoreContext(PERIOD_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const onPeriodToggle = React.useCallback(\n (period: Period) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n\n const currentHour =\n currentTime.hour !== undefined ? currentTime.hour : now.getHours();\n const currentDisplay = to12Hour(currentHour);\n const new24Hour = to24Hour(currentDisplay.hour, period);\n\n const newTime = { ...currentTime, hour: new24Hour };\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, store],\n );\n\n if (!is12Hour) return null;\n\n const now = new Date();\n const referenceHour = timeValue?.hour ?? now.getHours();\n const currentPeriod = to12Hour(referenceHour).period;\n\n const PeriodPrimitive = asChild ? Slot : TimePickerColumn;\n\n return (\n \n {PERIODS.map((period) => (\n onPeriodToggle(period)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerSeparatorProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction TimePickerSeparator(props: TimePickerSeparatorProps) {\n const { asChild, children, ...separatorProps } = props;\n\n const SeparatorPrimitive = asChild ? Slot : \"span\";\n\n return (\n \n {children ?? \":\"}\n \n );\n}\n\ninterface TimePickerClearProps extends ButtonProps {}\n\nfunction TimePickerClear(props: TimePickerClearProps) {\n const {\n asChild,\n className,\n children,\n disabled: disabledProp,\n ...clearProps\n } = props;\n\n const { disabled, readOnly } = useTimePickerContext(CLEAR_NAME);\n const store = useStoreContext(CLEAR_NAME);\n\n const isDisabled = disabledProp || disabled;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n clearProps.onClick?.(event);\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n if (disabled || readOnly) return;\n store.setState(\"value\", \"\");\n },\n [clearProps.onClick, disabled, readOnly, store],\n );\n\n const ClearPrimitive = asChild ? Slot : \"button\";\n\n return (\n \n {children ?? \"Clear\"}\n \n );\n}\n\nexport {\n TimePickerRoot as Root,\n TimePickerLabel as Label,\n TimePickerInputGroup as InputGroup,\n TimePickerInput as Input,\n TimePickerTrigger as Trigger,\n TimePickerContent as Content,\n TimePickerHour as Hour,\n TimePickerMinute as Minute,\n TimePickerSecond as Second,\n TimePickerPeriod as Period,\n TimePickerSeparator as Separator,\n TimePickerClear as Clear,\n //\n TimePickerRoot as TimePicker,\n TimePickerRoot,\n TimePickerLabel,\n TimePickerInputGroup,\n TimePickerInput,\n TimePickerTrigger,\n TimePickerContent,\n TimePickerHour,\n TimePickerMinute,\n TimePickerSecond,\n TimePickerPeriod,\n TimePickerSeparator,\n TimePickerClear,\n //\n type TimePickerRootProps as TimePickerProps,\n};\n", + "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Clock } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/default/components/visually-hidden-input\";\n\nconst ROOT_NAME = \"TimePicker\";\nconst LABEL_NAME = \"TimePickerLabel\";\nconst INPUT_GROUP_NAME = \"TimePickerInputGroup\";\nconst INPUT_NAME = \"TimePickerInput\";\nconst TRIGGER_NAME = \"TimePickerTrigger\";\nconst COLUMN_NAME = \"TimePickerColumn\";\nconst COLUMN_ITEM_NAME = \"TimePickerColumnItem\";\nconst HOUR_NAME = \"TimePickerHour\";\nconst MINUTE_NAME = \"TimePickerMinute\";\nconst SECOND_NAME = \"TimePickerSecond\";\nconst PERIOD_NAME = \"TimePickerPeriod\";\nconst CLEAR_NAME = \"TimePickerClear\";\n\nconst DEFAULT_STEP = 1;\nconst DEFAULT_SEGMENT_PLACEHOLDER = \"--\";\nconst DEFAULT_LOCALE = undefined;\nconst PERIODS = [\"AM\", \"PM\"] as const;\n\ntype Segment = \"hour\" | \"minute\" | \"second\" | \"period\";\ntype SegmentFormat = \"numeric\" | \"2-digit\";\ntype Period = (typeof PERIODS)[number];\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ninterface ButtonProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef;\ntype InputElement = React.ComponentRef;\ntype ColumnElement = React.ComponentRef;\ntype ColumnItemElement = React.ComponentRef;\n\ninterface TimeValue {\n hour?: number;\n minute?: number;\n second?: number;\n period?: Period;\n}\n\ninterface ItemData {\n value: number | string;\n ref: React.RefObject;\n selected: boolean;\n}\n\ninterface ColumnData {\n id: string;\n ref: React.RefObject;\n getSelectedItemRef: () => React.RefObject | null;\n getItems: () => ItemData[];\n}\n\nconst useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nfunction useAsRef(props: T) {\n const ref = React.useRef(props);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = props;\n });\n\n return ref;\n}\n\nfunction useLazyRef(fn: () => T) {\n const ref = React.useRef(null);\n\n if (ref.current === null) {\n ref.current = fn();\n }\n\n return ref as React.RefObject;\n}\n\nfunction focusFirst(\n candidates: React.RefObject[],\n preventScroll = false,\n) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidateRef of candidates) {\n const candidate = candidateRef.current;\n if (!candidate) continue;\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus({ preventScroll });\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\nfunction sortNodes }>(\n items: T[],\n): T[] {\n return items.sort((a, b) => {\n const elementA = a.ref.current;\n const elementB = b.ref.current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n return -1;\n }\n if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n return 1;\n }\n return 0;\n });\n}\n\nfunction getIs12Hour(locale?: string): boolean {\n const testDate = new Date(2000, 0, 1, 13, 0, 0);\n const formatted = new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n }).format(testDate);\n\n return /am|pm/i.test(formatted) || !formatted.includes(\"13\");\n}\n\nfunction parseTimeString(timeString: string | undefined): TimeValue | null {\n if (!timeString) return null;\n\n const parts = timeString.split(\":\");\n if (parts.length < 2) return null;\n\n const result: TimeValue = {};\n\n if (parts[0] && parts[0] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const hour = Number.parseInt(parts[0], 10);\n if (!Number.isNaN(hour) && hour >= 0 && hour <= 23) {\n result.hour = hour;\n }\n }\n\n if (parts[1] && parts[1] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const minute = Number.parseInt(parts[1], 10);\n if (!Number.isNaN(minute) && minute >= 0 && minute <= 59) {\n result.minute = minute;\n }\n }\n\n if (parts[2] && parts[2] !== DEFAULT_SEGMENT_PLACEHOLDER) {\n const second = Number.parseInt(parts[2], 10);\n if (!Number.isNaN(second) && second >= 0 && second <= 59) {\n result.second = second;\n }\n }\n if (\n result.hour === undefined &&\n result.minute === undefined &&\n result.second === undefined\n ) {\n return null;\n }\n\n return result;\n}\n\nfunction formatTimeValue(value: TimeValue, showSeconds: boolean): string {\n const hourStr =\n value.hour !== undefined\n ? value.hour.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n const minuteStr =\n value.minute !== undefined\n ? value.minute.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n const secondStr =\n value.second !== undefined\n ? value.second.toString().padStart(2, \"0\")\n : DEFAULT_SEGMENT_PLACEHOLDER;\n\n if (showSeconds) {\n return `${hourStr}:${minuteStr}:${secondStr}`;\n }\n return `${hourStr}:${minuteStr}`;\n}\n\nfunction to12Hour(hour24: number): { hour: number; period: Period } {\n const period: Period = hour24 >= 12 ? \"PM\" : \"AM\";\n const hour = hour24 % 12 || 12;\n return { hour, period };\n}\n\nfunction to24Hour(hour12: number, period: Period): number {\n if (hour12 === 12) {\n return period === \"PM\" ? 12 : 0;\n }\n return period === \"PM\" ? hour12 + 12 : hour12;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max);\n}\n\ninterface StoreState {\n value: string;\n open: boolean;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: (key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = useStoreContext(\"useStore\");\n\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ntype SegmentPlaceholder =\n | string\n | {\n hour?: string;\n minute?: string;\n second?: string;\n period?: string;\n };\n\ninterface TimePickerContextValue {\n id: string;\n inputGroupId: string;\n labelId: string;\n triggerId: string;\n disabled: boolean;\n readOnly: boolean;\n required: boolean;\n invalid: boolean;\n showSeconds: boolean;\n is12Hour: boolean;\n minuteStep: number;\n secondStep: number;\n hourStep: number;\n segmentPlaceholder: {\n hour: string;\n minute: string;\n second: string;\n period: string;\n };\n min?: string;\n max?: string;\n}\n\nconst TimePickerContext = React.createContext(\n null,\n);\n\nfunction useTimePickerContext(consumerName: string) {\n const context = React.useContext(TimePickerContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimePickerRootProps extends DivProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n min?: string;\n max?: string;\n hourStep?: number;\n minuteStep?: number;\n secondStep?: number;\n segmentPlaceholder?: SegmentPlaceholder;\n locale?: string;\n name?: string;\n disabled?: boolean;\n invalid?: boolean;\n readOnly?: boolean;\n required?: boolean;\n showSeconds?: boolean;\n}\n\nfunction TimePickerRoot(props: TimePickerRootProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n open,\n defaultOpen,\n onOpenChange,\n ...rootProps\n } = props;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n value: value ?? defaultValue ?? \"\",\n open: open ?? defaultOpen ?? false,\n }));\n const propsRef = useAsRef({ onValueChange, onOpenChange });\n\n const store: Store = React.useMemo(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n if (key === \"value\" && typeof value === \"string\") {\n stateRef.current.value = value;\n propsRef.current.onValueChange?.(value);\n } else if (key === \"open\" && typeof value === \"boolean\") {\n stateRef.current.open = value;\n propsRef.current.onOpenChange?.(value);\n } else {\n stateRef.current[key] = value;\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n return (\n \n \n \n );\n}\n\ninterface TimePickerRootImplProps\n extends Omit<\n TimePickerRootProps,\n \"defaultValue\" | \"defaultOpen\" | \"onValueChange\" | \"onOpenChange\"\n > {}\n\nfunction TimePickerRootImpl(props: TimePickerRootImplProps) {\n const {\n value,\n open: openProp,\n min,\n max,\n hourStep = DEFAULT_STEP,\n minuteStep = DEFAULT_STEP,\n secondStep = DEFAULT_STEP,\n segmentPlaceholder = DEFAULT_SEGMENT_PLACEHOLDER,\n locale = DEFAULT_LOCALE,\n name,\n asChild,\n disabled = false,\n invalid = false,\n readOnly = false,\n required = false,\n showSeconds = false,\n className,\n children,\n id,\n ref,\n ...rootProps\n } = props;\n\n const store = useStoreContext(\"TimePickerRootImpl\");\n\n useIsomorphicLayoutEffect(() => {\n if (value !== undefined) {\n store.setState(\"value\", value);\n }\n }, [value]);\n\n useIsomorphicLayoutEffect(() => {\n if (openProp !== undefined) {\n store.setState(\"open\", openProp);\n }\n }, [openProp]);\n\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n const inputGroupId = React.useId();\n const labelId = React.useId();\n const triggerId = React.useId();\n\n const [formTrigger, setFormTrigger] = React.useState(\n null,\n );\n const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n\n const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n const open = useStore((state) => state.open);\n\n const onPopoverOpenChange = React.useCallback(\n (newOpen: boolean) => store.setState(\"open\", newOpen),\n [store],\n );\n\n const is12Hour = React.useMemo(() => getIs12Hour(locale), [locale]);\n\n const normalizedPlaceholder = React.useMemo(() => {\n if (typeof segmentPlaceholder === \"string\") {\n return {\n hour: segmentPlaceholder,\n minute: segmentPlaceholder,\n second: segmentPlaceholder,\n period: segmentPlaceholder,\n };\n }\n return {\n hour: segmentPlaceholder.hour ?? DEFAULT_SEGMENT_PLACEHOLDER,\n minute: segmentPlaceholder.minute ?? DEFAULT_SEGMENT_PLACEHOLDER,\n second: segmentPlaceholder.second ?? DEFAULT_SEGMENT_PLACEHOLDER,\n period: segmentPlaceholder.period ?? DEFAULT_SEGMENT_PLACEHOLDER,\n };\n }, [segmentPlaceholder]);\n\n const rootContext = React.useMemo(\n () => ({\n id: rootId,\n inputGroupId,\n labelId,\n triggerId,\n disabled,\n readOnly,\n required,\n invalid,\n showSeconds,\n is12Hour,\n minuteStep,\n secondStep,\n hourStep,\n segmentPlaceholder: normalizedPlaceholder,\n min,\n max,\n }),\n [\n rootId,\n inputGroupId,\n labelId,\n triggerId,\n disabled,\n readOnly,\n required,\n invalid,\n showSeconds,\n is12Hour,\n minuteStep,\n secondStep,\n hourStep,\n normalizedPlaceholder,\n min,\n max,\n ],\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <>\n \n \n \n {children}\n \n \n \n {isFormControl && (\n \n )}\n \n );\n}\n\ninterface TimePickerLabelProps extends React.ComponentProps<\"label\"> {\n asChild?: boolean;\n}\n\nfunction TimePickerLabel(props: TimePickerLabelProps) {\n const { asChild, className, ...labelProps } = props;\n\n const { labelId } = useTimePickerContext(LABEL_NAME);\n\n const LabelPrimitive = asChild ? Slot : \"label\";\n\n return (\n \n );\n}\n\ninterface TimePickerInputGroupContextValue {\n onInputRegister: (\n segment: Segment,\n ref: React.RefObject,\n ) => void;\n onInputUnregister: (segment: Segment) => void;\n getNextInput: (\n currentSegment: Segment,\n ) => React.RefObject | null;\n}\n\nconst TimePickerInputGroupContext =\n React.createContext(null);\n\nfunction useTimePickerInputGroupContext(consumerName: string) {\n const context = React.useContext(TimePickerInputGroupContext);\n if (!context) {\n throw new Error(\n `\\`${consumerName}\\` must be used within \\`${INPUT_GROUP_NAME}\\``,\n );\n }\n return context;\n}\n\nfunction TimePickerInputGroup(props: DivProps) {\n const { asChild, className, style, ...inputGroupProps } = props;\n\n const { inputGroupId, labelId, disabled, invalid, segmentPlaceholder } =\n useTimePickerContext(INPUT_GROUP_NAME);\n\n const inputRefsMap = React.useRef<\n Map>\n >(new Map());\n\n const onInputRegister = React.useCallback(\n (segment: Segment, ref: React.RefObject) => {\n inputRefsMap.current.set(segment, ref);\n },\n [],\n );\n\n const onInputUnregister = React.useCallback((segment: Segment) => {\n inputRefsMap.current.delete(segment);\n }, []);\n\n const getNextInput = React.useCallback(\n (currentSegment: Segment): React.RefObject | null => {\n const segmentOrder: Segment[] = [\"hour\", \"minute\", \"second\", \"period\"];\n const currentIndex = segmentOrder.indexOf(currentSegment);\n\n if (currentIndex === -1 || currentIndex === segmentOrder.length - 1) {\n return null;\n }\n\n for (let i = currentIndex + 1; i < segmentOrder.length; i++) {\n const nextSegment = segmentOrder[i];\n if (nextSegment) {\n const nextRef = inputRefsMap.current.get(nextSegment);\n if (nextRef?.current) {\n return nextRef;\n }\n }\n }\n\n return null;\n },\n [],\n );\n\n const inputGroupContextValue =\n React.useMemo(\n () => ({\n onInputRegister,\n onInputUnregister,\n getNextInput,\n }),\n [onInputRegister, onInputUnregister, getNextInput],\n );\n\n const InputGroupPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n \n \n );\n}\n\ninterface TimePickerInputProps\n extends Omit, \"type\" | \"value\"> {\n segment?: Segment;\n}\n\nfunction TimePickerInput(props: TimePickerInputProps) {\n const {\n segment,\n disabled: disabledProp,\n readOnly: readOnlyProp,\n className,\n style,\n ref,\n onBlur: onBlurProp,\n onChange: onChangeProp,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n ...inputProps\n } = props;\n\n const { is12Hour, showSeconds, disabled, readOnly, segmentPlaceholder } =\n useTimePickerContext(INPUT_NAME);\n const store = useStoreContext(INPUT_NAME);\n const inputGroupContext = useTimePickerInputGroupContext(INPUT_NAME);\n\n const isDisabled = disabledProp || disabled;\n const isReadOnly = readOnlyProp || readOnly;\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const inputRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, inputRef);\n\n useIsomorphicLayoutEffect(() => {\n if (segment) {\n inputGroupContext.onInputRegister(segment as Segment, inputRef);\n return () => inputGroupContext.onInputUnregister(segment as Segment);\n }\n }, [inputGroupContext, segment]);\n\n const getSegmentValue = React.useCallback(() => {\n if (!timeValue) {\n if (!segment) return \"\";\n return segmentPlaceholder[segment];\n }\n switch (segment) {\n case \"hour\": {\n if (timeValue.hour === undefined) return segmentPlaceholder.hour;\n if (is12Hour) {\n return to12Hour(timeValue.hour).hour.toString().padStart(2, \"0\");\n }\n return timeValue.hour.toString().padStart(2, \"0\");\n }\n case \"minute\":\n if (timeValue.minute === undefined) return segmentPlaceholder.minute;\n return timeValue.minute.toString().padStart(2, \"0\");\n case \"second\":\n if (timeValue.second === undefined) return segmentPlaceholder.second;\n return timeValue.second.toString().padStart(2, \"0\");\n case \"period\": {\n if (!timeValue || timeValue.hour === undefined)\n return segmentPlaceholder.period;\n return to12Hour(timeValue.hour).period;\n }\n default:\n return \"\";\n }\n }, [timeValue, segment, is12Hour, segmentPlaceholder]);\n\n const [editValue, setEditValue] = React.useState(getSegmentValue());\n const [isEditing, setIsEditing] = React.useState(false);\n const [pendingDigit, setPendingDigit] = React.useState(null);\n\n React.useEffect(() => {\n if (!isEditing) {\n setEditValue(getSegmentValue());\n setPendingDigit(null);\n }\n }, [getSegmentValue, isEditing]);\n\n const updateTimeValue = React.useCallback(\n (newSegmentValue: string | undefined, shouldCreateIfEmpty = false) => {\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (!newSegmentValue || newSegmentValue === placeholder) return;\n if (!timeValue && !shouldCreateIfEmpty) return;\n\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime };\n\n switch (segment) {\n case \"hour\": {\n const displayHour = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(displayHour)) {\n if (is12Hour) {\n const clampedHour = clamp(displayHour, 1, 12);\n let currentPeriod: Period;\n if (timeValue?.period !== undefined) {\n currentPeriod = timeValue.period;\n } else if (timeValue?.hour !== undefined) {\n currentPeriod = to12Hour(timeValue.hour).period;\n } else {\n const now = new Date();\n currentPeriod = to12Hour(now.getHours()).period;\n }\n const hour24 = to24Hour(clampedHour, currentPeriod);\n newTime.hour = hour24;\n if (timeValue?.period !== undefined) {\n newTime.period = timeValue.period;\n }\n } else {\n newTime.hour = clamp(displayHour, 0, 23);\n }\n }\n break;\n }\n case \"minute\": {\n const minute = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(minute)) {\n newTime.minute = clamp(minute, 0, 59);\n }\n break;\n }\n case \"second\": {\n const second = Number.parseInt(newSegmentValue, 10);\n if (!Number.isNaN(second)) {\n newTime.second = clamp(second, 0, 59);\n }\n break;\n }\n case \"period\": {\n if (newSegmentValue === \"AM\" || newSegmentValue === \"PM\") {\n newTime.period = newSegmentValue;\n if (timeValue && timeValue.hour !== undefined) {\n const currentDisplay = to12Hour(timeValue.hour);\n newTime.hour = to24Hour(currentDisplay.hour, newSegmentValue);\n }\n }\n break;\n }\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, segment, is12Hour, showSeconds, store, segmentPlaceholder],\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent) => {\n onBlurProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsEditing(false);\n\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue && editValue !== placeholder && editValue.length > 0) {\n let valueToUpdate = editValue;\n\n if (segment !== \"period\") {\n if (editValue.length === 2) {\n valueToUpdate = editValue;\n } else if (editValue.length === 1) {\n const numValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(numValue)) {\n valueToUpdate = numValue.toString().padStart(2, \"0\");\n }\n }\n }\n\n updateTimeValue(valueToUpdate, true);\n\n queueMicrotask(() => {\n const currentTimeValue = parseTimeString(store.getState().value);\n if (currentTimeValue) {\n const now = new Date();\n const newTime = { ...currentTimeValue };\n let needsUpdate = false;\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n needsUpdate = true;\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n needsUpdate = true;\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n needsUpdate = true;\n }\n\n if (needsUpdate) {\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n }\n }\n });\n }\n\n setEditValue(getSegmentValue());\n setPendingDigit(null);\n },\n [\n onBlurProp,\n editValue,\n updateTimeValue,\n getSegmentValue,\n segment,\n segmentPlaceholder,\n showSeconds,\n store,\n ],\n );\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n onChangeProp?.(event);\n if (event.defaultPrevented) return;\n\n let newValue = event.target.value;\n\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (\n editValue === placeholder &&\n newValue.length > 0 &&\n newValue !== placeholder\n ) {\n newValue = newValue.replace(new RegExp(`^${placeholder}`), \"\");\n }\n\n if (segment === \"period\") {\n const firstChar = newValue.charAt(0).toUpperCase();\n let newPeriod: Period | null = null;\n\n if (firstChar === \"A\" || firstChar === \"1\") {\n newPeriod = \"AM\";\n } else if (firstChar === \"P\" || firstChar === \"2\") {\n newPeriod = \"PM\";\n }\n\n if (newPeriod) {\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n return;\n }\n\n if (segment === \"hour\" || segment === \"minute\" || segment === \"second\") {\n newValue = newValue.replace(/\\D/g, \"\");\n }\n\n if (newValue.length > 2) {\n newValue = newValue.slice(0, 2);\n }\n if (segment === \"hour\" || segment === \"minute\" || segment === \"second\") {\n const numValue = Number.parseInt(newValue, 10);\n\n if (!Number.isNaN(numValue) && newValue.length > 0) {\n if (pendingDigit !== null && newValue.length === 1) {\n const twoDigitValue = pendingDigit + newValue;\n const combinedNum = Number.parseInt(twoDigitValue, 10);\n\n if (!Number.isNaN(combinedNum)) {\n const paddedValue = combinedNum.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n return;\n }\n }\n\n const maxFirstDigit = segment === \"hour\" ? (is12Hour ? 1 : 2) : 5;\n\n const firstDigit = Number.parseInt(newValue[0] ?? \"0\", 10);\n const shouldAutoAdvance = firstDigit > maxFirstDigit;\n\n if (newValue.length === 1) {\n if (shouldAutoAdvance) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n } else {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n setPendingDigit(newValue);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n } else if (newValue.length === 2) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n setEditValue(paddedValue);\n updateTimeValue(paddedValue, true);\n setPendingDigit(null);\n\n queueMicrotask(() => {\n if (segment) {\n const nextInputRef = inputGroupContext.getNextInput(segment);\n if (nextInputRef?.current) {\n nextInputRef.current.focus();\n nextInputRef.current.select();\n }\n }\n });\n }\n } else if (newValue.length === 0) {\n setEditValue(\"\");\n setPendingDigit(null);\n }\n }\n },\n [\n segment,\n updateTimeValue,\n onChangeProp,\n editValue,\n is12Hour,\n inputGroupContext,\n pendingDigit,\n segmentPlaceholder,\n ],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n event.currentTarget.select();\n },\n [onClickProp],\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsEditing(true);\n setPendingDigit(null);\n queueMicrotask(() => event.target.select());\n },\n [onFocusProp],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n onKeyDownProp?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n event.preventDefault();\n\n const goToPrevious = event.key === \"ArrowLeft\";\n const inputGroup = inputRef.current?.closest(\n '[data-slot=\"time-picker-input-group\"]',\n );\n\n if (inputGroup && inputRef.current) {\n const allInputs = Array.from(\n inputGroup.querySelectorAll('input[type=\"text\"]'),\n ) as HTMLInputElement[];\n const currentIdx = allInputs.indexOf(inputRef.current);\n\n if (currentIdx !== -1) {\n const targetIdx = goToPrevious\n ? Math.max(0, currentIdx - 1)\n : Math.min(allInputs.length - 1, currentIdx + 1);\n\n const targetInput = allInputs[targetIdx];\n if (targetInput && targetInput !== inputRef.current) {\n targetInput.focus();\n targetInput.select();\n }\n }\n }\n return;\n }\n\n if (event.key === \"Backspace\" || event.key === \"Delete\") {\n const input = inputRef.current;\n if (\n input &&\n input.selectionStart === 0 &&\n input.selectionEnd === input.value.length\n ) {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n setEditValue(placeholder);\n setPendingDigit(null);\n\n if (timeValue) {\n const newTime = { ...timeValue };\n switch (segment) {\n case \"hour\":\n delete newTime.hour;\n break;\n case \"minute\":\n delete newTime.minute;\n break;\n case \"second\":\n delete newTime.second;\n break;\n case \"period\":\n delete newTime.period;\n break;\n }\n\n if (\n newTime.hour !== undefined ||\n newTime.minute !== undefined ||\n newTime.second !== undefined ||\n newTime.period !== undefined\n ) {\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n } else {\n store.setState(\"value\", \"\");\n }\n } else {\n store.setState(\"value\", \"\");\n }\n\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n return;\n }\n }\n\n if (segment === \"period\") {\n const key = event.key.toLowerCase();\n if (key === \"a\" || key === \"p\" || key === \"1\" || key === \"2\") {\n event.preventDefault();\n let newPeriod: Period;\n if (key === \"a\" || key === \"1\") {\n newPeriod = \"AM\";\n } else {\n newPeriod = \"PM\";\n }\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n } else if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n event.preventDefault();\n const placeholder = segmentPlaceholder.period;\n const currentPeriod =\n editValue === placeholder || editValue === \"\" ? \"AM\" : editValue;\n const newPeriod =\n currentPeriod === \"AM\" || currentPeriod === \"A\" ? \"PM\" : \"AM\";\n setEditValue(newPeriod);\n updateTimeValue(newPeriod, true);\n queueMicrotask(() => {\n inputRef.current?.select();\n });\n }\n return;\n }\n\n if (event.key === \"Tab\") {\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue && editValue.length > 0 && editValue !== placeholder) {\n if (editValue.length === 2) {\n updateTimeValue(editValue, true);\n } else if (editValue.length === 1) {\n const numValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(numValue)) {\n const paddedValue = numValue.toString().padStart(2, \"0\");\n updateTimeValue(paddedValue, true);\n }\n }\n }\n return;\n }\n\n if (event.key === \"Enter\") {\n event.preventDefault();\n inputRef.current?.blur();\n }\n\n if (event.key === \"Escape\") {\n event.preventDefault();\n setEditValue(getSegmentValue());\n inputRef.current?.blur();\n }\n\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue === placeholder || editValue === \"\") {\n const defaultValue = segment === \"hour\" ? (is12Hour ? 12 : 0) : 0;\n const formattedValue = defaultValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n return;\n }\n const currentValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(currentValue)) {\n let newValue: number;\n switch (segment) {\n case \"hour\":\n if (is12Hour) {\n newValue = currentValue === 12 ? 1 : currentValue + 1;\n } else {\n newValue = currentValue === 23 ? 0 : currentValue + 1;\n }\n break;\n case \"minute\":\n case \"second\":\n newValue = currentValue === 59 ? 0 : currentValue + 1;\n break;\n default:\n return;\n }\n const formattedValue = newValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n }\n }\n\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n const placeholder = segment\n ? segmentPlaceholder[segment]\n : DEFAULT_SEGMENT_PLACEHOLDER;\n if (editValue === placeholder || editValue === \"\") {\n const defaultValue = segment === \"hour\" ? (is12Hour ? 12 : 23) : 59;\n const formattedValue = defaultValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n return;\n }\n const currentValue = Number.parseInt(editValue, 10);\n if (!Number.isNaN(currentValue)) {\n let newValue: number;\n switch (segment) {\n case \"hour\":\n if (is12Hour) {\n newValue = currentValue === 1 ? 12 : currentValue - 1;\n } else {\n newValue = currentValue === 0 ? 23 : currentValue - 1;\n }\n break;\n case \"minute\":\n case \"second\":\n newValue = currentValue === 0 ? 59 : currentValue - 1;\n break;\n default:\n return;\n }\n const formattedValue = newValue.toString().padStart(2, \"0\");\n setEditValue(formattedValue);\n updateTimeValue(formattedValue, true);\n }\n }\n },\n [\n onKeyDownProp,\n editValue,\n segment,\n is12Hour,\n getSegmentValue,\n updateTimeValue,\n showSeconds,\n timeValue,\n store,\n segmentPlaceholder,\n ],\n );\n\n const displayValue = isEditing ? editValue : getSegmentValue();\n\n const segmentWidth = segment\n ? `var(--time-picker-${segment}-input-width)`\n : \"2ch\";\n\n return (\n \n );\n}\n\nfunction TimePickerTrigger(props: ButtonProps) {\n const {\n className,\n children,\n disabled: disabledProp,\n ...triggerProps\n } = props;\n\n const { triggerId, disabled } = useTimePickerContext(TRIGGER_NAME);\n\n const isDisabled = disabledProp || disabled;\n\n return (\n svg:not([class*='size-'])]:size-4\",\n className,\n )}\n >\n {children ?? }\n \n );\n}\n\ninterface TimePickerGroupContextValue {\n getColumns: () => ColumnData[];\n onColumnRegister: (column: ColumnData) => void;\n onColumnUnregister: (id: string) => void;\n}\n\nconst TimePickerGroupContext =\n React.createContext(null);\n\nfunction useTimePickerGroupContext(consumerName: string) {\n const context = React.useContext(TimePickerGroupContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimePickerContentProps\n extends DivProps,\n React.ComponentProps {}\n\nfunction TimePickerContent(props: TimePickerContentProps) {\n const {\n side = \"bottom\",\n align = \"start\",\n sideOffset = 4,\n className,\n onOpenAutoFocus: onOpenAutoFocusProp,\n children,\n ...contentProps\n } = props;\n\n const columnsRef = React.useRef>>(\n new Map(),\n );\n\n const onColumnRegister = React.useCallback((column: ColumnData) => {\n columnsRef.current.set(column.id, column);\n }, []);\n\n const onColumnUnregister = React.useCallback((id: string) => {\n columnsRef.current.delete(id);\n }, []);\n\n const getColumns = React.useCallback(() => {\n const columns = Array.from(columnsRef.current.entries())\n .map(([id, { ref, getSelectedItemRef, getItems }]) => ({\n id,\n ref,\n getSelectedItemRef,\n getItems,\n }))\n .filter((c) => c.ref.current !== null);\n return sortNodes(columns);\n }, []);\n\n const groupContextValue = React.useMemo(\n () => ({\n getColumns,\n onColumnRegister,\n onColumnUnregister,\n }),\n [getColumns, onColumnRegister, onColumnUnregister],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback(\n (event) => {\n onOpenAutoFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n const columns = getColumns();\n const firstColumn = columns[0];\n\n if (!firstColumn) return;\n\n const items = firstColumn.getItems();\n const selectedItem = items.find((item) => item.selected);\n\n const candidateRefs = selectedItem\n ? [selectedItem.ref, ...items.map((item) => item.ref)]\n : items.map((item) => item.ref);\n\n focusFirst(candidateRefs, false);\n },\n [onOpenAutoFocusProp, getColumns],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface TimePickerColumnContextValue {\n getItems: () => ItemData[];\n onItemRegister: (\n value: number | string,\n ref: React.RefObject,\n selected: boolean,\n ) => void;\n onItemUnregister: (value: number | string) => void;\n}\n\nconst TimePickerColumnContext =\n React.createContext(null);\n\nfunction useTimePickerColumnContext(consumerName: string) {\n const context = React.useContext(TimePickerColumnContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within a column`);\n }\n return context;\n}\n\ninterface TimePickerColumnProps extends DivProps {}\n\nfunction TimePickerColumn(props: TimePickerColumnProps) {\n const { children, className, ref, ...columnProps } = props;\n\n const columnId = React.useId();\n const columnRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, columnRef);\n\n const itemsRef = React.useRef<\n Map<\n number | string,\n {\n ref: React.RefObject;\n selected: boolean;\n }\n >\n >(new Map());\n\n const groupContext = useTimePickerGroupContext(COLUMN_NAME);\n\n const onItemRegister = React.useCallback(\n (\n value: number | string,\n ref: React.RefObject,\n selected: boolean,\n ) => {\n itemsRef.current.set(value, { ref, selected });\n },\n [],\n );\n\n const onItemUnregister = React.useCallback((value: number | string) => {\n itemsRef.current.delete(value);\n }, []);\n\n const getItems = React.useCallback(() => {\n const items = Array.from(itemsRef.current.entries())\n .map(([value, { ref, selected }]) => ({\n value,\n ref,\n selected,\n }))\n .filter((item) => item.ref.current);\n return sortNodes(items);\n }, []);\n\n const getSelectedItemRef = React.useCallback(() => {\n const items = getItems();\n return items.find((item) => item.selected)?.ref ?? null;\n }, [getItems]);\n\n useIsomorphicLayoutEffect(() => {\n groupContext.onColumnRegister({\n id: columnId,\n ref: columnRef,\n getSelectedItemRef,\n getItems,\n });\n return () => groupContext.onColumnUnregister(columnId);\n }, [groupContext, columnId, getSelectedItemRef, getItems]);\n\n const columnContextValue = React.useMemo(\n () => ({\n getItems,\n onItemRegister,\n onItemUnregister,\n }),\n [getItems, onItemRegister, onItemUnregister],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface TimePickerColumnItemProps extends ButtonProps {\n value: number | string;\n selected?: boolean;\n format?: SegmentFormat;\n}\n\nfunction TimePickerColumnItem(props: TimePickerColumnItemProps) {\n const {\n value,\n selected = false,\n format = \"numeric\",\n className,\n ref,\n ...itemProps\n } = props;\n\n const itemRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, itemRef);\n const columnContext = useTimePickerColumnContext(COLUMN_ITEM_NAME);\n const groupContext = useTimePickerGroupContext(COLUMN_ITEM_NAME);\n\n useIsomorphicLayoutEffect(() => {\n columnContext.onItemRegister(value, itemRef, selected);\n return () => columnContext.onItemUnregister(value);\n }, [columnContext, value, selected]);\n\n useIsomorphicLayoutEffect(() => {\n if (selected && itemRef.current) {\n itemRef.current.scrollIntoView({ block: \"nearest\" });\n }\n }, [selected]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n itemProps.onClick?.(event);\n if (event.defaultPrevented) return;\n\n itemRef.current?.focus();\n },\n [itemProps.onClick],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n itemProps.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n event.preventDefault();\n const items = columnContext.getItems().sort((a, b) => {\n if (typeof a.value === \"number\" && typeof b.value === \"number\") {\n return a.value - b.value;\n }\n return 0;\n });\n const currentIndex = items.findIndex((item) => item.value === value);\n\n let nextIndex: number;\n if (event.key === \"ArrowUp\") {\n nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;\n } else {\n nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;\n }\n\n const nextItem = items[nextIndex];\n nextItem?.ref.current?.focus();\n nextItem?.ref.current?.click();\n } else if (\n (event.key === \"Tab\" ||\n event.key === \"ArrowLeft\" ||\n event.key === \"ArrowRight\") &&\n groupContext\n ) {\n event.preventDefault();\n\n queueMicrotask(() => {\n const columns = groupContext.getColumns();\n\n if (columns.length === 0) return;\n\n const currentColumnIndex = columns.findIndex(\n (c) => c.ref.current?.contains(itemRef.current) ?? false,\n );\n\n if (currentColumnIndex === -1) return;\n\n const goToPrevious =\n event.key === \"ArrowLeft\" ||\n (event.key === \"Tab\" && event.shiftKey);\n\n const nextColumnIndex = goToPrevious\n ? currentColumnIndex > 0\n ? currentColumnIndex - 1\n : columns.length - 1\n : currentColumnIndex < columns.length - 1\n ? currentColumnIndex + 1\n : 0;\n\n const nextColumn = columns[nextColumnIndex];\n if (nextColumn?.ref.current) {\n const items = nextColumn.getItems();\n const selectedItem = items.find((item) => item.selected);\n\n const candidateRefs = selectedItem\n ? [selectedItem.ref, ...items.map((item) => item.ref)]\n : items.map((item) => item.ref);\n\n focusFirst(candidateRefs, false);\n }\n });\n }\n },\n [itemProps.onKeyDown, columnContext, groupContext, value],\n );\n\n const formattedValue =\n typeof value === \"number\" && format === \"2-digit\"\n ? value.toString().padStart(2, \"0\")\n : value.toString();\n\n return (\n \n {formattedValue}\n \n );\n}\n\ninterface TimePickerHourProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerHour(props: TimePickerHourProps) {\n const { asChild, format = \"numeric\", className, ...hourProps } = props;\n\n const { is12Hour, hourStep, showSeconds } = useTimePickerContext(HOUR_NAME);\n const store = useStoreContext(HOUR_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const hours = Array.from(\n {\n length: is12Hour ? Math.ceil(12 / hourStep) : Math.ceil(24 / hourStep),\n },\n (_, i) => {\n if (is12Hour) {\n const hour = (i * hourStep) % 12;\n return hour === 0 ? 12 : hour;\n }\n return i * hourStep;\n },\n );\n\n const onHourSelect = React.useCallback(\n (displayHour: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n\n let hour24 = displayHour;\n if (is12Hour) {\n let currentPeriod: Period;\n if (timeValue?.period !== undefined) {\n currentPeriod = timeValue.period;\n } else if (timeValue?.hour !== undefined) {\n currentPeriod = to12Hour(timeValue.hour).period;\n } else {\n currentPeriod = to12Hour(now.getHours()).period;\n }\n hour24 = to24Hour(displayHour, currentPeriod);\n }\n\n const newTime = { ...currentTime, hour: hour24 };\n if (timeValue && timeValue.period !== undefined) {\n newTime.period = timeValue.period;\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, is12Hour, store],\n );\n\n const now = new Date();\n const referenceHour = timeValue?.hour ?? now.getHours();\n const displayHour = is12Hour ? to12Hour(referenceHour).hour : referenceHour;\n\n const HourPrimitive = asChild ? Slot : TimePickerColumn;\n\n return (\n \n {hours.map((hour) => (\n onHourSelect(hour)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerMinuteProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerMinute(props: TimePickerMinuteProps) {\n const { asChild, format = \"2-digit\", className, ...minuteProps } = props;\n\n const { minuteStep, showSeconds } = useTimePickerContext(MINUTE_NAME);\n const store = useStoreContext(MINUTE_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const minutes = Array.from(\n { length: Math.ceil(60 / minuteStep) },\n (_, i) => i * minuteStep,\n );\n\n const onMinuteSelect = React.useCallback(\n (minute: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime, minute };\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, store],\n );\n\n const MinutePrimitive = asChild ? Slot : TimePickerColumn;\n\n const now = new Date();\n const referenceMinute = timeValue?.minute ?? now.getMinutes();\n\n return (\n \n {minutes.map((minute) => (\n onMinuteSelect(minute)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerSecondProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerSecond(props: TimePickerSecondProps) {\n const { asChild, format = \"2-digit\", className, ...secondProps } = props;\n\n const { secondStep } = useTimePickerContext(SECOND_NAME);\n const store = useStoreContext(SECOND_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const seconds = Array.from(\n { length: Math.ceil(60 / secondStep) },\n (_, i) => i * secondStep,\n );\n\n const onSecondSelect = React.useCallback(\n (second: number) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n const newTime = { ...currentTime, second };\n\n if (newTime.hour === undefined) {\n newTime.hour = now.getHours();\n }\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n const newValue = formatTimeValue(newTime, true);\n store.setState(\"value\", newValue);\n },\n [timeValue, store],\n );\n\n const SecondPrimitive = asChild ? Slot : TimePickerColumn;\n\n const now = new Date();\n const referenceSecond = timeValue?.second ?? now.getSeconds();\n\n return (\n \n {seconds.map((second) => (\n onSecondSelect(second)}\n />\n ))}\n \n );\n}\n\nfunction TimePickerPeriod(props: DivProps) {\n const { asChild, className, ...periodProps } = props;\n\n const { is12Hour, showSeconds } = useTimePickerContext(PERIOD_NAME);\n const store = useStoreContext(PERIOD_NAME);\n\n const value = useStore((state) => state.value);\n const timeValue = parseTimeString(value);\n\n const onPeriodToggle = React.useCallback(\n (period: Period) => {\n const now = new Date();\n const currentTime = timeValue ?? {};\n\n const currentHour =\n currentTime.hour !== undefined ? currentTime.hour : now.getHours();\n const currentDisplay = to12Hour(currentHour);\n const new24Hour = to24Hour(currentDisplay.hour, period);\n\n const newTime = { ...currentTime, hour: new24Hour };\n\n if (newTime.minute === undefined) {\n newTime.minute = now.getMinutes();\n }\n\n if (showSeconds && newTime.second === undefined) {\n newTime.second = now.getSeconds();\n }\n\n const newValue = formatTimeValue(newTime, showSeconds);\n store.setState(\"value\", newValue);\n },\n [timeValue, showSeconds, store],\n );\n\n if (!is12Hour) return null;\n\n const now = new Date();\n const referenceHour = timeValue?.hour ?? now.getHours();\n const currentPeriod = to12Hour(referenceHour).period;\n\n const PeriodPrimitive = asChild ? Slot : TimePickerColumn;\n\n return (\n \n {PERIODS.map((period) => (\n onPeriodToggle(period)}\n />\n ))}\n \n );\n}\n\ninterface TimePickerSeparatorProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction TimePickerSeparator(props: TimePickerSeparatorProps) {\n const { asChild, children, ...separatorProps } = props;\n\n const SeparatorPrimitive = asChild ? Slot : \"span\";\n\n return (\n \n {children ?? \":\"}\n \n );\n}\n\ninterface TimePickerClearProps extends ButtonProps {}\n\nfunction TimePickerClear(props: TimePickerClearProps) {\n const {\n asChild,\n className,\n children,\n disabled: disabledProp,\n ...clearProps\n } = props;\n\n const { disabled, readOnly } = useTimePickerContext(CLEAR_NAME);\n const store = useStoreContext(CLEAR_NAME);\n\n const isDisabled = disabledProp || disabled;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n clearProps.onClick?.(event);\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n if (disabled || readOnly) return;\n store.setState(\"value\", \"\");\n },\n [clearProps.onClick, disabled, readOnly, store],\n );\n\n const ClearPrimitive = asChild ? Slot : \"button\";\n\n return (\n \n {children ?? \"Clear\"}\n \n );\n}\n\nexport {\n TimePickerRoot as Root,\n TimePickerLabel as Label,\n TimePickerInputGroup as InputGroup,\n TimePickerInput as Input,\n TimePickerTrigger as Trigger,\n TimePickerContent as Content,\n TimePickerHour as Hour,\n TimePickerMinute as Minute,\n TimePickerSecond as Second,\n TimePickerPeriod as Period,\n TimePickerSeparator as Separator,\n TimePickerClear as Clear,\n //\n TimePickerRoot as TimePicker,\n TimePickerRoot,\n TimePickerLabel,\n TimePickerInputGroup,\n TimePickerInput,\n TimePickerTrigger,\n TimePickerContent,\n TimePickerHour,\n TimePickerMinute,\n TimePickerSecond,\n TimePickerPeriod,\n TimePickerSeparator,\n TimePickerClear,\n //\n type TimePickerRootProps as TimePickerProps,\n};\n", "type": "registry:ui", "target": "" }, diff --git a/docs/registry/default/ui/time-picker.tsx b/docs/registry/default/ui/time-picker.tsx index 81aef1b3..c1528b9d 100644 --- a/docs/registry/default/ui/time-picker.tsx +++ b/docs/registry/default/ui/time-picker.tsx @@ -718,10 +718,11 @@ function TimePickerInput(props: TimePickerInputProps) { case "second": if (timeValue.second === undefined) return segmentPlaceholder.second; return timeValue.second.toString().padStart(2, "0"); - case "period": + case "period": { if (!timeValue || timeValue.hour === undefined) return segmentPlaceholder.period; return to12Hour(timeValue.hour).period; + } default: return ""; } @@ -755,8 +756,17 @@ function TimePickerInput(props: TimePickerInputProps) { if (!Number.isNaN(displayHour)) { if (is12Hour) { const clampedHour = clamp(displayHour, 1, 12); - const currentPeriod = timeValue?.period || "AM"; - newTime.hour = to24Hour(clampedHour, currentPeriod); + let currentPeriod: Period; + if (timeValue?.period !== undefined) { + currentPeriod = timeValue.period; + } else if (timeValue?.hour !== undefined) { + currentPeriod = to12Hour(timeValue.hour).period; + } else { + const now = new Date(); + currentPeriod = to12Hour(now.getHours()).period; + } + const hour24 = to24Hour(clampedHour, currentPeriod); + newTime.hour = hour24; if (timeValue?.period !== undefined) { newTime.period = timeValue.period; } @@ -1711,7 +1721,14 @@ function TimePickerHour(props: TimePickerHourProps) { let hour24 = displayHour; if (is12Hour) { - const currentPeriod = timeValue?.period || "AM"; + let currentPeriod: Period; + if (timeValue?.period !== undefined) { + currentPeriod = timeValue.period; + } else if (timeValue?.hour !== undefined) { + currentPeriod = to12Hour(timeValue.hour).period; + } else { + currentPeriod = to12Hour(now.getHours()).period; + } hour24 = to24Hour(displayHour, currentPeriod); }