{ "name": "time-picker", "type": "registry:ui", "files": [ { "path": "ui/time-picker.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { Clock } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/bases/base/components/visually-hidden-input\";\nimport { useAsRef } from \"@/registry/bases/base/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/base/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { useComposedRefs } from \"@/registry/bases/base/lib/compose-refs\";\nimport { Popover, PopoverTrigger } from \"@/registry/bases/base/ui/popover\";\n\nconst ROOT_NAME = \"TimePicker\";\nconst LABEL_NAME = \"TimePickerLabel\";\nconst INPUT_GROUP_NAME = \"TimePickerInputGroup\";\nconst INPUT_NAME = \"TimePickerInput\";\nconst TRIGGER_NAME = \"TimePickerTrigger\";\nconst CONTENT_NAME = \"TimePickerContent\";\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 SEGMENTS: Segment[] = [\"hour\", \"minute\", \"second\", \"period\"];\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\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\ninterface ButtonProps\n extends React.ComponentProps<\"button\">,\n useRender.ComponentProps<\"button\"> {}\n\ntype InputGroupElement = HTMLDivElement;\ntype InputElement = HTMLInputElement;\ntype TriggerElement = HTMLButtonElement;\ntype ColumnElement = HTMLDivElement;\ntype ColumnItemElement = HTMLButtonElement;\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\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 openedViaFocus: 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(\n selector: (state: StoreState) => T,\n ogStore?: Store | null,\n): T {\n const contextStore = React.useContext(StoreContext);\n\n const store = ogStore ?? contextStore;\n\n if (!store) {\n throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n }\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 inputGroupRef: React.RefObject;\n triggerRef: React.RefObject;\n openOnFocus: boolean;\n inputGroupClickAction: \"focus\" | \"open\";\n onInputGroupChange: (inputGroup: InputGroupElement | null) => void;\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 TimePickerProps 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 openOnFocus?: boolean;\n inputGroupClickAction?: \"focus\" | \"open\";\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 TimePicker(props: TimePickerProps) {\n const {\n value: valueProp,\n defaultValue,\n onValueChange,\n open,\n defaultOpen,\n onOpenChange,\n openOnFocus = false,\n inputGroupClickAction = \"focus\",\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 render,\n disabled = false,\n invalid = false,\n readOnly = false,\n required = false,\n showSeconds = false,\n className,\n children,\n id,\n ...rootProps\n } = props;\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 inputGroupRef = React.useRef(null);\n const triggerRef = React.useRef(null);\n\n const [inputGroup, setInputGroup] = React.useState(\n null,\n );\n const isFormControl = inputGroup ? !!inputGroup.closest(\"form\") : true;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n value: valueProp ?? defaultValue ?? \"\",\n open: open ?? defaultOpen ?? false,\n openedViaFocus: false,\n }));\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 if (!value) {\n stateRef.current.openedViaFocus = false;\n }\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 const value = useStore((state) => state.value, store);\n\n useIsomorphicLayoutEffect(() => {\n if (valueProp !== undefined) {\n store.setState(\"value\", valueProp);\n }\n }, [valueProp]);\n\n useIsomorphicLayoutEffect(() => {\n if (open !== undefined) {\n store.setState(\"open\", open);\n }\n }, [open]);\n\n const storeOpen = useStore((state) => state.open, store);\n\n const onPopoverOpenChange = React.useCallback(\n (\n newOpen: boolean,\n eventDetails: PopoverPrimitive.Root.ChangeEventDetails,\n ) => {\n if (!newOpen && eventDetails.reason === \"outside-press\") {\n if (eventDetails.isCanceled) return;\n\n if (openOnFocus && inputGroupRef.current) {\n const target = eventDetails.event.target;\n if (\n target instanceof Node &&\n inputGroupRef.current.contains(target)\n ) {\n eventDetails.cancel();\n return;\n }\n }\n }\n\n store.setState(\"open\", newOpen);\n },\n [store, openOnFocus],\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 inputGroupRef,\n triggerRef,\n openOnFocus,\n inputGroupClickAction,\n onInputGroupChange: setInputGroup,\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 openOnFocus,\n inputGroupClickAction,\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 element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\"relative\", className),\n children,\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"time-picker\",\n disabled: disabled ? \"\" : undefined,\n invalid: invalid ? \"\" : undefined,\n },\n });\n\n return (\n <>\n \n \n \n {element}\n \n \n \n {isFormControl && (\n \n )}\n \n );\n}\n\ninterface TimePickerLabelProps\n extends React.ComponentProps<\"label\">,\n useRender.ComponentProps<\"label\"> {}\n\nfunction TimePickerLabel(props: TimePickerLabelProps) {\n const { render, className, ...labelProps } = props;\n\n const { labelId } = useTimePickerContext(LABEL_NAME);\n\n return useRender({\n defaultTagName: \"label\",\n props: mergeProps<\"label\">(\n {\n htmlFor: labelId,\n className: cn(\n \"font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n className,\n ),\n },\n labelProps,\n ),\n render,\n state: {\n slot: \"time-picker-label\",\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 {\n onPointerDown: onPointerDownProp,\n onClick: onClickProp,\n render,\n className,\n style,\n ref,\n ...inputGroupProps\n } = props;\n\n const {\n inputGroupId,\n labelId,\n onInputGroupChange,\n disabled,\n readOnly,\n invalid,\n segmentPlaceholder,\n inputGroupRef,\n triggerRef,\n inputGroupClickAction,\n } = useTimePickerContext(INPUT_GROUP_NAME);\n\n const store = useStoreContext(INPUT_GROUP_NAME);\n\n const composedRef = useComposedRefs(ref, inputGroupRef, onInputGroupChange);\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 onPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n onPointerDownProp?.(event);\n if (disabled || readOnly || event.defaultPrevented) return;\n\n const target = event.target as HTMLElement;\n\n if (target.tagName === \"INPUT\" || target.closest(\"input\")) {\n return;\n }\n\n if (triggerRef.current?.contains(target)) {\n return;\n }\n\n event.preventDefault();\n },\n [onPointerDownProp, disabled, readOnly, triggerRef],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n if (disabled || readOnly || event.defaultPrevented) return;\n\n const target = event.target as HTMLElement;\n\n if (target.tagName === \"INPUT\" || target.closest(\"input\")) {\n return;\n }\n\n if (triggerRef.current?.contains(target)) {\n return;\n }\n\n if (inputGroupClickAction === \"open\") {\n store.setState(\"open\", true);\n } else {\n const activeElement = document.activeElement;\n const isInputAlreadyFocused =\n activeElement &&\n activeElement.tagName === \"INPUT\" &&\n inputGroupRef.current?.contains(activeElement);\n\n if (!isInputAlreadyFocused) {\n for (const segment of SEGMENTS) {\n const inputRef = inputRefsMap.current.get(segment);\n if (inputRef?.current) {\n inputRef.current.focus();\n inputRef.current.select();\n break;\n }\n }\n }\n }\n },\n [\n onClickProp,\n disabled,\n readOnly,\n inputGroupClickAction,\n store,\n triggerRef,\n inputGroupRef,\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 element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"group\",\n id: inputGroupId,\n \"aria-labelledby\": labelId,\n ref: composedRef,\n className: cn(\n \"flex h-10 w-full cursor-text items-center gap-0.5 rounded-md border border-input bg-background px-3 py-2 shadow-xs outline-none transition-shadow\",\n \"has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50\",\n invalid && \"border-destructive ring-destructive/20\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n ),\n style: {\n \"--time-picker-hour-input-width\": `${segmentPlaceholder.hour.length}ch`,\n \"--time-picker-minute-input-width\": `${segmentPlaceholder.minute.length}ch`,\n \"--time-picker-second-input-width\": `${segmentPlaceholder.second.length}ch`,\n \"--time-picker-period-input-width\": `${Math.max(segmentPlaceholder.period.length, 2) + 0.5}ch`,\n ...style,\n } as React.CSSProperties,\n onPointerDown,\n onClick,\n },\n inputGroupProps,\n ),\n render,\n state: {\n slot: \"time-picker-input-group\",\n disabled: disabled ? \"\" : undefined,\n invalid: invalid ? \"\" : undefined,\n },\n });\n\n return (\n \n {element}\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 onBlur: onBlurProp,\n onChange: onChangeProp,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n disabled: disabledProp,\n readOnly: readOnlyProp,\n className,\n style,\n ref,\n ...inputProps\n } = props;\n\n const {\n is12Hour,\n showSeconds,\n disabled,\n readOnly,\n segmentPlaceholder,\n openOnFocus,\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\n if (openOnFocus && !store.getState().open) {\n store.setState(\"openedViaFocus\", true);\n store.setState(\"open\", true);\n }\n\n queueMicrotask(() => event.target.select());\n },\n [onFocusProp, openOnFocus, store],\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 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 queueMicrotask(() => {\n inputRef.current?.select();\n });\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 queueMicrotask(() => {\n inputRef.current?.select();\n });\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 queueMicrotask(() => {\n inputRef.current?.select();\n });\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 queueMicrotask(() => {\n inputRef.current?.select();\n });\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 queueMicrotask(() => {\n inputRef.current?.select();\n });\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 ref,\n ...triggerProps\n } = props;\n\n const { triggerId, disabled, triggerRef } =\n useTimePickerContext(TRIGGER_NAME);\n\n const isDisabled = disabledProp || disabled;\n\n const composedRef = useComposedRefs(ref, triggerRef);\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 PopoverPrimitive.Popup.Props,\n Pick<\n PopoverPrimitive.Positioner.Props,\n \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n > {}\n\nfunction TimePickerContent(props: TimePickerContentProps) {\n const {\n side = \"bottom\",\n align = \"start\",\n sideOffset = 6,\n alignOffset = 0,\n className,\n initialFocus: initialFocusProp,\n ...contentProps\n } = props;\n\n const store = useStoreContext(CONTENT_NAME);\n const { inputGroupRef } = useTimePickerContext(CONTENT_NAME);\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 onInitialFocus = React.useCallback(\n (openType: \"mouse\" | \"touch\" | \"pen\" | \"keyboard\" | \"\") => {\n if (typeof initialFocusProp === \"function\") {\n const resolved = initialFocusProp(openType);\n if (resolved !== undefined) return resolved;\n }\n\n const { openedViaFocus } = store.getState();\n\n if (openedViaFocus) {\n store.setState(\"openedViaFocus\", false);\n return false;\n }\n\n const columns = getColumns();\n const firstColumn = columns[0];\n\n if (!firstColumn) return false;\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 return false;\n },\n [getColumns, initialFocusProp, store],\n );\n\n const resolvedInitialFocus =\n typeof initialFocusProp === \"boolean\" ||\n (initialFocusProp !== undefined && typeof initialFocusProp !== \"function\")\n ? initialFocusProp\n : onInitialFocus;\n\n return (\n \n \n \n \n \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 { render, 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 hoursContent = hours.map((hour) => (\n onHourSelect(hour)}\n />\n ));\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1\",\n className,\n ),\n children: hoursContent,\n },\n hourProps,\n ),\n render: render || undefined,\n state: {\n slot: \"time-picker-hour\",\n },\n });\n\n if (render) {\n return element;\n }\n\n return (\n \n {hoursContent}\n \n );\n}\n\ninterface TimePickerMinuteProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerMinute(props: TimePickerMinuteProps) {\n const { render, 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 now = new Date();\n const referenceMinute = timeValue?.minute ?? now.getMinutes();\n\n const minutesContent = minutes.map((minute) => (\n onMinuteSelect(minute)}\n />\n ));\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1\",\n className,\n ),\n children: minutesContent,\n },\n minuteProps,\n ),\n render: render || undefined,\n state: {\n slot: \"time-picker-minute\",\n },\n });\n\n if (render) {\n return element;\n }\n\n return (\n \n {minutesContent}\n \n );\n}\n\ninterface TimePickerSecondProps extends DivProps {\n format?: SegmentFormat;\n}\n\nfunction TimePickerSecond(props: TimePickerSecondProps) {\n const { render, 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 now = new Date();\n const referenceSecond = timeValue?.second ?? now.getSeconds();\n\n const secondsContent = seconds.map((second) => (\n onSecondSelect(second)}\n />\n ));\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1\",\n className,\n ),\n children: secondsContent,\n },\n secondProps,\n ),\n render: render || undefined,\n state: {\n slot: \"time-picker-second\",\n },\n });\n\n if (render) {\n return element;\n }\n\n return (\n \n {secondsContent}\n \n );\n}\n\nfunction TimePickerPeriod(props: DivProps) {\n const { render, 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 const now = new Date();\n const referenceHour = timeValue?.hour ?? now.getHours();\n const currentPeriod = to12Hour(referenceHour).period;\n\n const periodsContent = PERIODS.map((period) => (\n onPeriodToggle(period)}\n />\n ));\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\"flex flex-col gap-1 p-1\", className),\n children: periodsContent,\n },\n periodProps,\n ),\n render: render || undefined,\n state: {\n slot: \"time-picker-period\",\n },\n });\n\n if (!is12Hour) return null;\n\n if (render) {\n return element;\n }\n\n return (\n \n {periodsContent}\n \n );\n}\n\ninterface TimePickerSeparatorProps\n extends React.ComponentProps<\"span\">,\n useRender.ComponentProps<\"span\"> {}\n\nfunction TimePickerSeparator(props: TimePickerSeparatorProps) {\n const { render, children, ...separatorProps } = props;\n\n return useRender({\n defaultTagName: \"span\",\n props: mergeProps<\"span\">(\n {\n \"aria-hidden\": \"true\",\n children: children ?? \":\",\n },\n separatorProps,\n ),\n render,\n state: {\n slot: \"time-picker-separator\",\n },\n });\n}\n\nfunction TimePickerClear(props: ButtonProps) {\n const {\n render,\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 return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: \"button\",\n disabled: isDisabled,\n className: cn(\n \"inline-flex items-center justify-center rounded-sm font-medium text-sm transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\",\n className,\n ),\n onClick,\n children: children ?? \"Clear\",\n },\n clearProps,\n ),\n render,\n state: {\n slot: \"time-picker-clear\",\n },\n });\n}\n\nexport {\n TimePicker,\n TimePickerClear,\n TimePickerContent,\n TimePickerHour,\n TimePickerInput,\n TimePickerInputGroup,\n TimePickerLabel,\n TimePickerMinute,\n TimePickerPeriod,\n type TimePickerProps,\n TimePickerSecond,\n TimePickerSeparator,\n TimePickerTrigger,\n useStore as useTimePicker,\n};\n", "target": "" }, { "path": "components/visually-hidden-input.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype InputValue = string[] | string;\n\ninterface VisuallyHiddenInputProps\n extends Omit<\n React.InputHTMLAttributes,\n \"value\" | \"checked\" | \"onReset\"\n > {\n value?: T;\n checked?: boolean;\n control: HTMLElement | null;\n bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput(\n props: VisuallyHiddenInputProps,\n) {\n const {\n control,\n value,\n checked,\n bubbles = true,\n type = \"hidden\",\n style,\n ...inputProps\n } = props;\n\n const isCheckInput = React.useMemo(\n () => type === \"checkbox\" || type === \"radio\" || type === \"switch\",\n [type],\n );\n const inputRef = React.useRef(null);\n\n const prevValueRef = React.useRef<{\n value: T | boolean | undefined;\n previous: T | boolean | undefined;\n }>({\n value: isCheckInput ? checked : value,\n previous: isCheckInput ? checked : value,\n });\n\n const prevValue = React.useMemo(() => {\n const currentValue = isCheckInput ? checked : value;\n if (prevValueRef.current.value !== currentValue) {\n prevValueRef.current.previous = prevValueRef.current.value;\n prevValueRef.current.value = currentValue;\n }\n return prevValueRef.current.previous;\n }, [isCheckInput, value, checked]);\n\n const [controlSize, setControlSize] = React.useState<{\n width?: number;\n height?: number;\n }>({});\n\n React.useLayoutEffect(() => {\n if (!control) {\n setControlSize({});\n return;\n }\n\n setControlSize({\n width: control.offsetWidth,\n height: control.offsetHeight,\n });\n\n if (typeof window === \"undefined\") return;\n\n const resizeObserver = new ResizeObserver((entries) => {\n if (!Array.isArray(entries) || !entries.length) return;\n\n const entry = entries[0];\n if (!entry) return;\n\n let width: number;\n let height: number;\n\n if (\"borderBoxSize\" in entry) {\n const borderSizeEntry = entry.borderBoxSize;\n const borderSize = Array.isArray(borderSizeEntry)\n ? borderSizeEntry[0]\n : borderSizeEntry;\n width = borderSize.inlineSize;\n height = borderSize.blockSize;\n } else {\n width = control.offsetWidth;\n height = control.offsetHeight;\n }\n\n setControlSize({ width, height });\n });\n\n resizeObserver.observe(control, { box: \"border-box\" });\n return () => {\n resizeObserver.disconnect();\n };\n }, [control]);\n\n React.useEffect(() => {\n const input = inputRef.current;\n if (!input) return;\n\n const inputProto = window.HTMLInputElement.prototype;\n const propertyKey = isCheckInput ? \"checked\" : \"value\";\n const eventType = isCheckInput ? \"click\" : \"input\";\n const currentValue = isCheckInput ? checked : value;\n\n const serializedCurrentValue = isCheckInput\n ? checked\n : typeof value === \"object\" && value !== null\n ? JSON.stringify(value)\n : value;\n\n const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);\n\n const setter = descriptor?.set;\n\n if (prevValue !== currentValue && setter) {\n const event = new Event(eventType, { bubbles });\n setter.call(input, serializedCurrentValue);\n input.dispatchEvent(event);\n }\n }, [prevValue, value, checked, bubbles, isCheckInput]);\n\n const composedStyle = React.useMemo(() => {\n return {\n ...style,\n ...(controlSize.width !== undefined && controlSize.height !== undefined\n ? controlSize\n : {}),\n border: 0,\n clip: \"rect(0 0 0 0)\",\n clipPath: \"inset(50%)\",\n height: \"1px\",\n margin: \"-1px\",\n overflow: \"hidden\",\n padding: 0,\n position: \"absolute\",\n whiteSpace: \"nowrap\",\n width: \"1px\",\n };\n }, [style, controlSize]);\n\n return (\n \n );\n}\n\nexport { VisuallyHiddenInput };\n", "target": "" }, { "path": "lib/compose-refs.ts", "type": "registry:lib", "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "popover", "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react" ], "css": { "@utility scrollbar-none": { "scrollbar-width": "none", "&::-webkit-scrollbar": { "display": "none" } } } }