{ "name": "angle-slider", "type": "registry:ui", "dependencies": [ "radix-ui" ], "registryDependencies": [ "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "files": [ { "path": "ui/angle-slider.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport {\n Direction as DirectionPrimitive,\n Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/bases/radix/components/visually-hidden-input\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"AngleSlider\";\nconst THUMB_NAME = \"AngleSliderThumb\";\n\nconst PAGE_KEYS = [\"PageUp\", \"PageDown\"];\nconst ARROW_KEYS = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"];\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef;\ntype ThumbElement = React.ComponentRef;\n\nfunction clamp(value: number, [min, max]: [number, number]) {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction getNextSortedValues(\n prevValues: number[] = [],\n nextValue: number,\n atIndex: number,\n) {\n const nextValues = [...prevValues];\n nextValues[atIndex] = nextValue;\n return nextValues.sort((a, b) => a - b);\n}\n\nfunction getStepsBetweenValues(values: number[]) {\n return values.slice(0, -1).map((value, index) => {\n const nextValue = values[index + 1];\n return nextValue !== undefined ? nextValue - value : 0;\n });\n}\n\nfunction hasMinStepsBetweenValues(\n values: number[],\n minStepsBetweenValues: number,\n) {\n if (minStepsBetweenValues > 0) {\n const stepsBetweenValues = getStepsBetweenValues(values);\n const actualMinStepsBetweenValues =\n stepsBetweenValues.length > 0 ? Math.min(...stepsBetweenValues) : 0;\n return actualMinStepsBetweenValues >= minStepsBetweenValues;\n }\n return true;\n}\n\nfunction getDecimalCount(value: number) {\n return (String(value).split(\".\")[1] ?? \"\").length;\n}\n\nfunction roundValue(value: number, decimalCount: number) {\n const rounder = 10 ** decimalCount;\n return Math.round(value * rounder) / rounder;\n}\n\nfunction getClosestValueIndex(values: number[], nextValue: number) {\n if (values.length === 1) return 0;\n const distances = values.map((value) => Math.abs(value - nextValue));\n const closestDistance = Math.min(...distances);\n return distances.indexOf(closestDistance);\n}\n\ninterface ThumbData {\n id: string;\n element: ThumbElement;\n index: number;\n value: number;\n}\n\ninterface StoreState {\n values: number[];\n thumbs: Map;\n valueIndexToChange: number;\n min: number;\n max: number;\n step: number;\n size: number;\n thickness: number;\n startAngle: number;\n endAngle: number;\n minStepsBetweenThumbs: number;\n disabled: boolean;\n inverted: 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 addThumb: (index: number, thumbData: ThumbData) => void;\n removeThumb: (index: number) => void;\n updateValue: (\n value: number,\n atIndex: number,\n options?: { commit?: boolean },\n ) => void;\n getValueFromPointer: (\n clientX: number,\n clientY: number,\n rect: DOMRect,\n ) => number;\n getAngleFromValue: (value: number) => number;\n getPositionFromAngle: (angle: number) => { x: number; y: number };\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\ninterface SliderContextValue {\n dir: Direction;\n name?: string;\n form?: string;\n}\n\nconst SliderContext = React.createContext(null);\n\nfunction useSliderContext(consumerName: string) {\n const context = React.useContext(SliderContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface AngleSliderProps extends Omit {\n value?: number[];\n defaultValue?: number[];\n onValueChange?: (value: number[]) => void;\n onValueCommit?: (value: number[]) => void;\n min?: number;\n max?: number;\n step?: number;\n minStepsBetweenThumbs?: number;\n size?: number;\n thickness?: number;\n startAngle?: number;\n endAngle?: number;\n dir?: Direction;\n form?: string;\n name?: string;\n disabled?: boolean;\n inverted?: boolean;\n}\n\nfunction AngleSlider(props: AngleSliderProps) {\n const {\n value,\n defaultValue = [0],\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n minStepsBetweenThumbs = 0,\n size = 60,\n thickness = 8,\n startAngle = -90,\n endAngle = 270,\n dir: dirProp,\n form,\n name,\n disabled = false,\n inverted = false,\n asChild,\n className,\n ref,\n onPointerMove: onPointerMoveProp,\n onPointerUp: onPointerUpProp,\n onPointerDown: onPointerDownProp,\n onKeyDown: onKeyDownProp,\n ...rootProps\n } = props;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n values: value ?? defaultValue,\n thumbs: new Map(),\n valueIndexToChange: 0,\n min,\n max,\n step,\n minStepsBetweenThumbs,\n disabled,\n inverted,\n size,\n thickness,\n startAngle,\n endAngle,\n }));\n\n const propsRef = useAsRef({\n onValueChange,\n onValueCommit,\n onPointerMove: onPointerMoveProp,\n onPointerUp: onPointerUpProp,\n onPointerDown: onPointerDownProp,\n onKeyDown: onKeyDownProp,\n });\n\n const 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 === \"values\" && Array.isArray(value)) {\n const hasChanged = String(stateRef.current.values) !== String(value);\n stateRef.current.values = value;\n if (hasChanged) {\n propsRef.current.onValueChange?.(value);\n }\n } else {\n stateRef.current[key] = value;\n }\n\n store.notify();\n },\n addThumb: (index, thumbData) => {\n stateRef.current.thumbs.set(index, thumbData);\n store.notify();\n },\n removeThumb: (index) => {\n stateRef.current.thumbs.delete(index);\n store.notify();\n },\n updateValue: (value, atIndex, { commit = false } = {}) => {\n const { min, max, step, minStepsBetweenThumbs } = stateRef.current;\n const decimalCount = getDecimalCount(step);\n const snapToStep = roundValue(\n Math.round((value - min) / step) * step + min,\n decimalCount,\n );\n const nextValue = clamp(snapToStep, [min, max]);\n\n const nextValues = getNextSortedValues(\n stateRef.current.values,\n nextValue,\n atIndex,\n );\n\n if (\n hasMinStepsBetweenValues(nextValues, minStepsBetweenThumbs * step)\n ) {\n stateRef.current.valueIndexToChange = nextValues.indexOf(nextValue);\n const hasChanged =\n String(nextValues) !== String(stateRef.current.values);\n\n if (hasChanged) {\n stateRef.current.values = nextValues;\n propsRef.current.onValueChange?.(nextValues);\n if (commit) propsRef.current.onValueCommit?.(nextValues);\n store.notify();\n }\n }\n },\n getValueFromPointer: (clientX, clientY, rect) => {\n const { min, max, inverted, startAngle, endAngle } = stateRef.current;\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n\n const deltaX = clientX - centerX;\n const deltaY = clientY - centerY;\n let angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;\n\n if (angle < 0) angle += 360;\n\n angle = (angle - startAngle + 360) % 360;\n\n const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n\n let percent = angle / totalAngle;\n if (inverted) percent = 1 - percent;\n\n return min + percent * (max - min);\n },\n getAngleFromValue: (value) => {\n const { min, max, inverted, startAngle, endAngle } = stateRef.current;\n let percent = (value - min) / (max - min);\n if (inverted) percent = 1 - percent;\n\n const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n const angle = startAngle + percent * totalAngle;\n\n return angle;\n },\n getPositionFromAngle: (angle) => {\n const { size } = stateRef.current;\n const radians = (angle * Math.PI) / 180;\n\n return {\n x: size * Math.cos(radians),\n y: size * Math.sin(radians),\n };\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n useIsomorphicLayoutEffect(() => {\n if (value !== undefined) {\n store.setState(\"values\", value);\n }\n }, [value, store]);\n\n useIsomorphicLayoutEffect(() => {\n const currentState = store.getState();\n\n if (currentState.min !== min) {\n store.setState(\"min\", min);\n }\n if (currentState.max !== max) {\n store.setState(\"max\", max);\n }\n if (currentState.step !== step) {\n store.setState(\"step\", step);\n }\n if (currentState.minStepsBetweenThumbs !== minStepsBetweenThumbs) {\n store.setState(\"minStepsBetweenThumbs\", minStepsBetweenThumbs);\n }\n if (currentState.size !== size) {\n store.setState(\"size\", size);\n }\n if (currentState.thickness !== thickness) {\n store.setState(\"thickness\", thickness);\n }\n if (currentState.startAngle !== startAngle) {\n store.setState(\"startAngle\", startAngle);\n }\n if (currentState.endAngle !== endAngle) {\n store.setState(\"endAngle\", endAngle);\n }\n if (currentState.disabled !== disabled) {\n store.setState(\"disabled\", disabled);\n }\n if (currentState.inverted !== inverted) {\n store.setState(\"inverted\", inverted);\n }\n }, [\n store,\n min,\n max,\n step,\n minStepsBetweenThumbs,\n size,\n thickness,\n startAngle,\n endAngle,\n disabled,\n inverted,\n ]);\n\n const dir = DirectionPrimitive.useDirection(dirProp);\n\n const [sliderElement, setSliderElement] = React.useState(\n null,\n );\n const composedRef = useComposedRefs(ref, setSliderElement);\n const valuesBeforeSlideStartRef = React.useRef(value ?? defaultValue);\n\n const contextValue = React.useMemo(\n () => ({\n dir,\n name,\n form,\n }),\n [dir, name, form],\n );\n\n const onSliderStart = React.useCallback(\n (pointerValue: number) => {\n if (disabled) return;\n\n const values = store.getState().values;\n const closestIndex = getClosestValueIndex(values, pointerValue);\n store.setState(\"valueIndexToChange\", closestIndex);\n store.updateValue(pointerValue, closestIndex);\n },\n [store, disabled],\n );\n\n const onSliderMove = React.useCallback(\n (pointerValue: number) => {\n if (disabled) return;\n\n const valueIndexToChange = store.getState().valueIndexToChange;\n store.updateValue(pointerValue, valueIndexToChange);\n },\n [store, disabled],\n );\n\n const onSliderEnd = React.useCallback(() => {\n if (disabled) return;\n\n const state = store.getState();\n const prevValue =\n valuesBeforeSlideStartRef.current[state.valueIndexToChange];\n const nextValue = state.values[state.valueIndexToChange];\n const hasChanged = nextValue !== prevValue;\n\n if (hasChanged) {\n onValueCommit?.(state.values);\n }\n }, [store, disabled, onValueCommit]);\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n propsRef.current.onKeyDown?.(event);\n if (event.defaultPrevented || disabled) return;\n\n const state = store.getState();\n const { values, valueIndexToChange, min, max, step } = state;\n const currentValue = values[valueIndexToChange] ?? min;\n\n if (event.key === \"Home\") {\n event.preventDefault();\n store.updateValue(min, 0, { commit: true });\n } else if (event.key === \"End\") {\n event.preventDefault();\n store.updateValue(max, values.length - 1, { commit: true });\n } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) {\n event.preventDefault();\n\n const isPageKey = PAGE_KEYS.includes(event.key);\n const isSkipKey =\n isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key));\n const multiplier = isSkipKey ? 10 : 1;\n\n let direction = 0;\n const isDecreaseKey = [\"ArrowLeft\", \"ArrowUp\", \"PageUp\"].includes(\n event.key,\n );\n direction = isDecreaseKey ? -1 : 1;\n if (inverted) direction *= -1;\n\n const stepInDirection = step * multiplier * direction;\n store.updateValue(currentValue + stepInDirection, valueIndexToChange, {\n commit: true,\n });\n }\n },\n [store, propsRef, disabled, inverted],\n );\n\n const onPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n propsRef.current.onPointerDown?.(event);\n if (event.defaultPrevented || disabled) return;\n\n const target = event.target as HTMLElement;\n target.setPointerCapture(event.pointerId);\n event.preventDefault();\n\n if (!disabled) {\n valuesBeforeSlideStartRef.current = store.getState().values;\n\n const thumbs = Array.from(store.getState().thumbs.values());\n const clickedThumb = thumbs.find((thumb) =>\n thumb.element.contains(target),\n );\n\n if (clickedThumb) {\n clickedThumb.element.focus();\n store.setState(\"valueIndexToChange\", clickedThumb.index);\n } else if (sliderElement) {\n const rect = sliderElement.getBoundingClientRect();\n const pointerValue = store.getValueFromPointer(\n event.clientX,\n event.clientY,\n rect,\n );\n onSliderStart(pointerValue);\n }\n }\n },\n [store, propsRef, disabled, sliderElement, onSliderStart],\n );\n\n const onPointerMove = React.useCallback(\n (event: React.PointerEvent) => {\n propsRef.current.onPointerMove?.(event);\n if (event.defaultPrevented || disabled) return;\n\n const target = event.target as HTMLElement;\n if (target.hasPointerCapture(event.pointerId) && sliderElement) {\n const rect = sliderElement.getBoundingClientRect();\n const pointerValue = store.getValueFromPointer(\n event.clientX,\n event.clientY,\n rect,\n );\n onSliderMove(pointerValue);\n }\n },\n [store, propsRef, disabled, sliderElement, onSliderMove],\n );\n\n const onPointerUp = React.useCallback(\n (event: React.PointerEvent) => {\n propsRef.current.onPointerUp?.(event);\n if (event.defaultPrevented) return;\n\n const target = event.target as RootElement;\n if (target.hasPointerCapture(event.pointerId)) {\n target.releasePointerCapture(event.pointerId);\n onSliderEnd();\n }\n },\n [propsRef, onSliderEnd],\n );\n\n const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n \n \n \n );\n}\n\nfunction AngleSliderTrack(props: React.ComponentProps<\"svg\">) {\n const { className, children, ...trackProps } = props;\n\n const disabled = useStore((state) => state.disabled);\n const size = useStore((state) => state.size);\n const thickness = useStore((state) => state.thickness);\n const startAngle = useStore((state) => state.startAngle);\n const endAngle = useStore((state) => state.endAngle);\n\n const center = size + 20;\n const trackRadius = size;\n\n const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n const isFullCircle = totalAngle >= 359;\n\n const startRadians = (startAngle * Math.PI) / 180;\n const endRadians = (endAngle * Math.PI) / 180;\n\n const startX = center + trackRadius * Math.cos(startRadians);\n const startY = center + trackRadius * Math.sin(startRadians);\n const endX = center + trackRadius * Math.cos(endRadians);\n const endY = center + trackRadius * Math.sin(endRadians);\n\n const largeArcFlag = totalAngle > 180 ? 1 : 0;\n\n return (\n \n {isFullCircle ? (\n \n ) : (\n \n )}\n {children}\n \n );\n}\n\nfunction AngleSliderRange(props: React.ComponentProps<\"path\">) {\n const { className, ...rangeProps } = props;\n\n const values = useStore((state) => state.values);\n const min = useStore((state) => state.min);\n const max = useStore((state) => state.max);\n const disabled = useStore((state) => state.disabled);\n const size = useStore((state) => state.size);\n const thickness = useStore((state) => state.thickness);\n const startAngle = useStore((state) => state.startAngle);\n const endAngle = useStore((state) => state.endAngle);\n\n const center = size + 20;\n const trackRadius = size;\n\n const sortedValues = [...values].sort((a, b) => a - b);\n\n const rangeStart = values.length <= 1 ? min : (sortedValues[0] ?? min);\n const rangeEnd =\n values.length <= 1\n ? (sortedValues[0] ?? min)\n : (sortedValues[sortedValues.length - 1] ?? max);\n\n const rangeStartPercent = (rangeStart - min) / (max - min);\n const rangeEndPercent = (rangeEnd - min) / (max - min);\n\n const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n const rangeStartAngle = startAngle + rangeStartPercent * totalAngle;\n const rangeEndAngle = startAngle + rangeEndPercent * totalAngle;\n\n const rangeStartRadians = (rangeStartAngle * Math.PI) / 180;\n const rangeEndRadians = (rangeEndAngle * Math.PI) / 180;\n\n const startX = center + trackRadius * Math.cos(rangeStartRadians);\n const startY = center + trackRadius * Math.sin(rangeStartRadians);\n const endX = center + trackRadius * Math.cos(rangeEndRadians);\n const endY = center + trackRadius * Math.sin(rangeEndRadians);\n\n const rangeAngle = (rangeEndAngle - rangeStartAngle + 360) % 360;\n const largeArcFlag = rangeAngle > 180 ? 1 : 0;\n\n if (rangeStart === rangeEnd) return null;\n\n return (\n \n );\n}\n\ninterface AngleSliderThumbProps extends DivProps {\n index?: number;\n}\n\nfunction AngleSliderThumb(props: AngleSliderThumbProps) {\n const { index: indexProp, className, asChild, ref, ...thumbProps } = props;\n\n const context = useSliderContext(THUMB_NAME);\n const store = useStoreContext(THUMB_NAME);\n const values = useStore((state) => state.values);\n const min = useStore((state) => state.min);\n const max = useStore((state) => state.max);\n const step = useStore((state) => state.step);\n const disabled = useStore((state) => state.disabled);\n const size = useStore((state) => state.size);\n\n const thumbId = React.useId();\n const [thumbElement, setThumbElement] = React.useState(\n null,\n );\n const composedRef = useComposedRefs(ref, setThumbElement);\n\n const isFormControl = thumbElement\n ? context.form || !!thumbElement.closest(\"form\")\n : true;\n\n const index = indexProp ?? 0;\n const value = values[index];\n\n React.useEffect(() => {\n if (thumbElement && value !== undefined) {\n store.addThumb(index, {\n id: thumbId,\n element: thumbElement,\n index,\n value,\n });\n\n return () => {\n store.removeThumb(index);\n };\n }\n }, [thumbElement, thumbId, index, value, store]);\n\n const thumbStyle = React.useMemo(() => {\n if (value === undefined) return {};\n\n const angle = store.getAngleFromValue(value);\n const position = store.getPositionFromAngle(angle);\n const center = size + 20;\n\n return {\n position: \"absolute\",\n left: `${center + position.x}px`,\n top: `${center + position.y}px`,\n transform: \"translate(-50%, -50%)\",\n };\n }, [value, store, size]);\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n props.onFocus?.(event);\n if (event.defaultPrevented) return;\n\n store.setState(\"valueIndexToChange\", index);\n },\n [props.onFocus, store, index],\n );\n\n const ThumbPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n if (value === undefined) return null;\n\n return (\n \n \n {isFormControl && value !== undefined && (\n 1 ? \"[]\" : \"\")\n : undefined\n }\n form={context.form}\n value={value.toString()}\n type=\"number\"\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n />\n )}\n \n );\n}\n\ninterface AngleSliderValueProps extends DivProps {\n unit?: string;\n formatValue?: (value: number | number[]) => string;\n}\n\nfunction AngleSliderValue(props: AngleSliderValueProps) {\n const {\n unit = \"°\",\n formatValue,\n className,\n style,\n asChild,\n children,\n ...valueProps\n } = props;\n\n const values = useStore((state) => state.values);\n const size = useStore((state) => state.size);\n const disabled = useStore((state) => state.disabled);\n\n const center = size + 20;\n\n const displayValue = React.useMemo(() => {\n if (formatValue) {\n return formatValue(values.length === 1 ? (values[0] ?? 0) : values);\n }\n\n if (values.length === 1) {\n return `${values[0] ?? 0}${unit}`;\n }\n\n const sortedValues = [...values].sort((a, b) => a - b);\n return `${sortedValues[0]}${unit} - ${sortedValues[sortedValues.length - 1]}${unit}`;\n }, [values, formatValue, unit]);\n\n const valueStyle = React.useMemo(\n () => ({\n position: \"absolute\",\n left: `${center}px`,\n top: `${center}px`,\n transform: \"translate(-50%, -50%)\",\n }),\n [center],\n );\n\n const ValuePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n {children ?? displayValue}\n \n );\n}\n\nexport {\n AngleSlider,\n type AngleSliderProps,\n AngleSliderRange,\n AngleSliderThumb,\n AngleSliderTrack,\n AngleSliderValue,\n useStore as useAngleSlider,\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": "" } ] }