{ "name": "rating", "type": "registry:ui", "dependencies": [ "radix-ui" ], "registryDependencies": [ "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "files": [ { "path": "ui/rating.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { Star } from \"lucide-react\";\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\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\ntype ActivationMode = \"automatic\" | \"manual\";\ntype Size = \"default\" | \"sm\" | \"lg\";\ntype Step = 0.5 | 1;\ntype DataState = \"full\" | \"partial\" | \"empty\";\ntype FocusIntent = \"first\" | \"last\" | \"prev\" | \"next\";\n\ntype RootElement = React.ComponentRef;\ntype ItemElement = React.ComponentRef;\n\nconst ROOT_NAME = \"Rating\";\nconst ITEM_NAME = \"RatingItem\";\n\nconst ENTRY_FOCUS = \"ratingFocusGroup.onEntryFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\nfunction getItemId(id: string, value: number) {\n return `${id}-item-${value}`;\n}\n\nfunction getPartialFillGradientId(id: string, step: Step) {\n return `partial-fill-gradient-${id}-${step}`;\n}\n\nconst MAP_KEY_TO_FOCUS_INTENT: Record = {\n ArrowLeft: \"prev\",\n ArrowUp: \"prev\",\n ArrowRight: \"next\",\n ArrowDown: \"next\",\n Home: \"first\",\n End: \"last\",\n};\n\nfunction getDirectionAwareKey(key: string, dir?: Direction) {\n if (dir !== \"rtl\") return key;\n return key === \"ArrowLeft\"\n ? \"ArrowRight\"\n : key === \"ArrowRight\"\n ? \"ArrowLeft\"\n : key;\n}\n\nfunction getFocusIntent(\n event: React.KeyboardEvent,\n dir?: Direction,\n orientation?: Orientation,\n) {\n const key = getDirectionAwareKey(event.key, dir);\n if (orientation === \"horizontal\" && [\"ArrowUp\", \"ArrowDown\"].includes(key))\n return undefined;\n if (orientation === \"vertical\" && [\"ArrowLeft\", \"ArrowRight\"].includes(key))\n return undefined;\n return MAP_KEY_TO_FOCUS_INTENT[key];\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\ninterface StoreState {\n value: number;\n hoveredValue: number | null;\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\ninterface ItemData {\n id: string;\n ref: React.RefObject;\n value: number;\n disabled: boolean;\n}\n\ninterface RatingContextValue {\n rootId: string;\n dir: Direction;\n orientation: Orientation;\n activationMode: ActivationMode;\n size: Size;\n max: number;\n step: Step;\n clearable: boolean;\n disabled: boolean;\n readOnly: boolean;\n getAutoIndex: (instanceId: string) => number;\n}\n\nconst RatingContext = React.createContext(null);\n\nfunction useRatingContext(consumerName: string) {\n const context = React.useContext(RatingContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface FocusContextValue {\n tabStopId: string | null;\n onItemFocus: (tabStopId: string) => void;\n onItemShiftTab: () => void;\n onFocusableItemAdd: () => void;\n onFocusableItemRemove: () => void;\n onItemRegister: (item: ItemData) => void;\n onItemUnregister: (id: string) => void;\n getItems: () => ItemData[];\n}\n\nconst FocusContext = React.createContext(null);\n\nfunction useFocusContext(consumerName: string) {\n const context = React.useContext(FocusContext);\n if (!context) {\n throw new Error(\n `\\`${consumerName}\\` must be used within \\`FocusProvider\\``,\n );\n }\n return context;\n}\n\ninterface RatingProps extends React.ComponentProps<\"div\"> {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n onHover?: (value: number | null) => void;\n max?: number;\n activationMode?: ActivationMode;\n dir?: Direction;\n orientation?: Orientation;\n size?: Size;\n asChild?: boolean;\n step?: Step;\n clearable?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n required?: boolean;\n name?: string;\n}\n\nfunction Rating(props: RatingProps) {\n const {\n value: valueProp,\n defaultValue = 0,\n onValueChange,\n onHover,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n dir: dirProp,\n orientation = \"horizontal\",\n activationMode = \"automatic\",\n size = \"default\",\n max = 5,\n step = 1,\n clearable = false,\n asChild,\n disabled = false,\n readOnly = false,\n required = false,\n className,\n id,\n name,\n ref,\n ...rootProps\n } = props;\n\n const dir = DirectionPrimitive.useDirection(dirProp);\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n value: valueProp ?? defaultValue,\n hoveredValue: null,\n }));\n\n const propsRef = useAsRef({\n onValueChange,\n onHover,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n step,\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 === \"value\" && typeof value === \"number\") {\n stateRef.current.value = value;\n propsRef.current.onValueChange?.(value);\n } else if (key === \"hoveredValue\") {\n stateRef.current.hoveredValue = value as number | null;\n propsRef.current.onHover?.(value as number | null);\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 useIsomorphicLayoutEffect(() => {\n if (valueProp !== undefined) {\n store.setState(\"value\", valueProp);\n }\n }, [valueProp]);\n\n const value = useStore((state) => state.value, store);\n\n const [formTrigger, setFormTrigger] = React.useState(\n null,\n );\n const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n const [tabStopId, setTabStopId] = React.useState(null);\n const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);\n const [focusableItemCount, setFocusableItemCount] = React.useState(0);\n const isClickFocusRef = React.useRef(false);\n const itemsRef = React.useRef>(new Map());\n\n const autoIndexMapRef = React.useRef(new Map());\n const nextAutoIndexRef = React.useRef(0);\n\n const getAutoIndex = React.useCallback((instanceId: string) => {\n const existingIndex = autoIndexMapRef.current.get(instanceId);\n if (existingIndex !== undefined) {\n return existingIndex;\n }\n\n const newIndex = nextAutoIndexRef.current++;\n autoIndexMapRef.current.set(instanceId, newIndex);\n return newIndex;\n }, []);\n\n const onItemFocus = React.useCallback((tabStopId: string) => {\n setTabStopId(tabStopId);\n }, []);\n\n const onItemShiftTab = React.useCallback(() => {\n setIsTabbingBackOut(true);\n }, []);\n\n const onFocusableItemAdd = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount + 1);\n }, []);\n\n const onFocusableItemRemove = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount - 1);\n }, []);\n\n const onItemRegister = React.useCallback((item: ItemData) => {\n itemsRef.current.set(item.id, item);\n }, []);\n\n const onItemUnregister = React.useCallback((id: string) => {\n itemsRef.current.delete(id);\n }, []);\n\n const getItems = React.useCallback(() => {\n return Array.from(itemsRef.current.values())\n .filter((item) => item.ref.current)\n .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\n const onBlur = React.useCallback(\n (event: React.FocusEvent) => {\n rootProps.onBlur?.(event);\n if (event.defaultPrevented) return;\n\n setIsTabbingBackOut(false);\n },\n [rootProps.onBlur],\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n propsRef.current.onFocus?.(event);\n if (event.defaultPrevented) return;\n\n const isKeyboardFocus = !isClickFocusRef.current;\n if (\n event.target === event.currentTarget &&\n isKeyboardFocus &&\n !isTabbingBackOut\n ) {\n const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);\n event.currentTarget.dispatchEvent(entryFocusEvent);\n\n if (!entryFocusEvent.defaultPrevented) {\n const items = Array.from(itemsRef.current.values()).filter(\n (item) => !item.disabled,\n );\n // For half-step ratings, find the item that represents the selected value\n // by looking for the ceiling value (e.g., 3.5 → find item with value 4)\n const selectedItem =\n propsRef.current.step < 1\n ? items.find((item) => item.value === Math.ceil(value))\n : items.find((item) => item.value === value);\n const currentItem = items.find((item) => item.id === tabStopId);\n\n const candidateItems = [selectedItem, currentItem, ...items].filter(\n Boolean,\n ) as ItemData[];\n const candidateRefs = candidateItems.map((item) => item.ref);\n focusFirst(candidateRefs, false);\n }\n }\n isClickFocusRef.current = false;\n },\n [propsRef, isTabbingBackOut, value, tabStopId],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseDown?.(event);\n\n if (event.defaultPrevented) return;\n\n isClickFocusRef.current = true;\n },\n [propsRef],\n );\n\n const contextValue = React.useMemo(\n () => ({\n rootId,\n dir,\n orientation,\n activationMode,\n disabled,\n readOnly,\n size,\n max,\n step,\n clearable,\n getAutoIndex,\n }),\n [\n rootId,\n dir,\n orientation,\n activationMode,\n disabled,\n readOnly,\n size,\n max,\n step,\n clearable,\n getAutoIndex,\n ],\n );\n\n const focusContextValue = React.useMemo(\n () => ({\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems,\n }),\n [\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems,\n ],\n );\n\n const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n \n \n \n \n \n {dir === \"rtl\" ? (\n <>\n \n \n \n ) : (\n <>\n \n \n \n )}\n \n \n \n {isFormControl && (\n \n )}\n \n \n \n );\n}\n\ninterface RatingItemProps\n extends Omit, \"children\"> {\n index?: number;\n asChild?: boolean;\n children?: React.ReactNode | ((dataState: DataState) => React.ReactNode);\n}\n\nfunction RatingItem(props: RatingItemProps) {\n const {\n index,\n asChild,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n onMouseEnter: onMouseEnterProp,\n onMouseMove: onMouseMoveProp,\n onMouseLeave: onMouseLeaveProp,\n disabled,\n className,\n children,\n ref,\n ...itemProps\n } = props;\n\n const itemRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, itemRef);\n\n const context = useRatingContext(ITEM_NAME);\n\n const instanceId = React.useId();\n\n const actualIndex = React.useMemo(() => {\n if (index !== undefined) {\n return index;\n }\n\n return context.getAutoIndex(instanceId);\n }, [index, context, instanceId]);\n\n const itemValue = actualIndex + 1;\n const store = useStoreContext(ITEM_NAME);\n const focusContext = useFocusContext(ITEM_NAME);\n const value = useStore((state) => state.value);\n const hoveredValue = useStore((state) => state.hoveredValue);\n const clearable = context.clearable;\n const step = context.step;\n const activationMode = context.activationMode;\n\n const itemId = getItemId(context.rootId, itemValue);\n const isDisabled = context.disabled || disabled;\n const isReadOnly = context.readOnly;\n const isTabStop = focusContext.tabStopId === itemId;\n\n const displayValue = hoveredValue ?? value;\n const isFilled = displayValue >= itemValue;\n const isPartiallyFilled =\n step < 1 && displayValue >= itemValue - step && displayValue < itemValue;\n const isHovered = hoveredValue !== null && hoveredValue < itemValue;\n\n const isMouseClickRef = React.useRef(false);\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n onMouseEnter: onMouseEnterProp,\n onMouseMove: onMouseMoveProp,\n onMouseLeave: onMouseLeaveProp,\n });\n\n useIsomorphicLayoutEffect(() => {\n focusContext.onItemRegister({\n id: itemId,\n ref: itemRef,\n value: itemValue,\n disabled: !!isDisabled,\n });\n\n if (!isDisabled) {\n focusContext.onFocusableItemAdd();\n }\n\n return () => {\n focusContext.onItemUnregister(itemId);\n if (!isDisabled) {\n focusContext.onFocusableItemRemove();\n }\n };\n }, [focusContext, itemId, itemValue, isDisabled]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n if (event.defaultPrevented) return;\n\n if (!isDisabled && !isReadOnly) {\n let newValue = itemValue;\n\n if (step < 1) {\n const rect = event.currentTarget.getBoundingClientRect();\n const clickX = event.clientX - rect.left;\n const isLeftHalf = clickX < rect.width / 2;\n\n if (context.dir === \"rtl\") {\n if (!isLeftHalf) {\n newValue = itemValue - step;\n }\n } else {\n if (isLeftHalf) {\n newValue = itemValue - step;\n }\n }\n }\n\n if (clearable && value === newValue) {\n newValue = 0;\n }\n\n store.setState(\"value\", newValue);\n }\n },\n [\n isDisabled,\n isReadOnly,\n clearable,\n step,\n value,\n itemValue,\n store,\n context.dir,\n propsRef,\n ],\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n propsRef.current.onFocus?.(event);\n if (event.defaultPrevented) return;\n\n focusContext.onItemFocus(itemId);\n\n const isKeyboardFocus = !isMouseClickRef.current;\n\n if (\n !isDisabled &&\n !isReadOnly &&\n activationMode !== \"manual\" &&\n isKeyboardFocus\n ) {\n // For half-step mode, check if the current value is a half-step that belongs to this item\n // e.g., if value is 3.5 and itemValue is 4, don't change it\n const isHalfStepValue = step < 1 && value === itemValue - step;\n\n if (!isHalfStepValue) {\n const newValue = clearable && value === itemValue ? 0 : itemValue;\n store.setState(\"value\", newValue);\n }\n }\n\n isMouseClickRef.current = false;\n },\n [\n focusContext,\n itemId,\n activationMode,\n isDisabled,\n isReadOnly,\n clearable,\n value,\n itemValue,\n step,\n store,\n propsRef,\n ],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n propsRef.current.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (\n (event.key === \"Enter\" || event.key === \" \") &&\n activationMode === \"manual\"\n ) {\n event.preventDefault();\n if (!isDisabled && !isReadOnly && itemRef.current) {\n itemRef.current.click();\n }\n return;\n }\n\n if (event.key === \"Tab\" && event.shiftKey) {\n focusContext.onItemShiftTab();\n return;\n }\n\n if (event.target !== event.currentTarget) return;\n\n const focusIntent = getFocusIntent(\n event,\n context.dir,\n context.orientation,\n );\n\n if (focusIntent !== undefined) {\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)\n return;\n event.preventDefault();\n\n // For half-step mode, increment/decrement by step value instead of jumping to next item\n if (step < 1 && (focusIntent === \"prev\" || focusIntent === \"next\")) {\n if (!isDisabled && !isReadOnly) {\n let newValue = value;\n\n if (focusIntent === \"next\") {\n newValue = Math.min(value + step, context.max);\n } else {\n newValue = Math.max(value - step, 0);\n }\n\n store.setState(\"value\", newValue);\n\n // Find and focus the item that represents this value\n const items = focusContext\n .getItems()\n .filter((item) => !item.disabled);\n const targetItem = items.find(\n (item) => item.value === Math.ceil(newValue),\n );\n if (targetItem?.ref.current) {\n queueMicrotask(() => targetItem.ref.current?.focus());\n }\n }\n return;\n }\n\n // For full-step mode or Home/End keys, use the original navigation\n const items = focusContext.getItems().filter((item) => !item.disabled);\n let candidateRefs = items.map((item) => item.ref);\n\n if (focusIntent === \"last\") {\n candidateRefs.reverse();\n } else if (focusIntent === \"prev\" || focusIntent === \"next\") {\n if (focusIntent === \"prev\") candidateRefs.reverse();\n const currentIndex = candidateRefs.findIndex(\n (ref) => ref.current === event.currentTarget,\n );\n candidateRefs = candidateRefs.slice(currentIndex + 1);\n }\n\n queueMicrotask(() => focusFirst(candidateRefs));\n }\n },\n [\n focusContext,\n context.dir,\n context.orientation,\n activationMode,\n isDisabled,\n isReadOnly,\n step,\n value,\n context.max,\n store,\n propsRef,\n ],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseDown?.(event);\n if (event.defaultPrevented) return;\n\n isMouseClickRef.current = true;\n\n if (isDisabled) {\n event.preventDefault();\n } else {\n focusContext.onItemFocus(itemId);\n }\n },\n [focusContext, itemId, isDisabled, propsRef],\n );\n\n const onMouseEnter = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseEnter?.(event);\n if (event.defaultPrevented) return;\n\n if (!isDisabled && !isReadOnly) {\n let hoverValue = itemValue;\n\n if (step < 1) {\n const rect = event.currentTarget.getBoundingClientRect();\n const mouseX = event.clientX - rect.left;\n const isLeftHalf = mouseX < rect.width / 2;\n\n if (context.dir === \"rtl\") {\n if (!isLeftHalf) {\n hoverValue = itemValue - step;\n }\n } else {\n if (isLeftHalf) {\n hoverValue = itemValue - step;\n }\n }\n }\n\n store.setState(\"hoveredValue\", hoverValue);\n }\n },\n [isDisabled, isReadOnly, step, itemValue, store, context.dir, propsRef],\n );\n\n const onMouseLeave = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseLeave?.(event);\n if (event.defaultPrevented) return;\n\n if (!isDisabled && !isReadOnly) {\n store.setState(\"hoveredValue\", null);\n }\n },\n [isDisabled, isReadOnly, store, propsRef],\n );\n\n const onMouseMove = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseMove?.(event);\n if (event.defaultPrevented) return;\n\n if (!isDisabled && !isReadOnly && step < 1) {\n const rect = event.currentTarget.getBoundingClientRect();\n const mouseX = event.clientX - rect.left;\n const isLeftHalf = mouseX < rect.width / 2;\n\n let hoverValue = itemValue;\n if (context.dir === \"rtl\") {\n hoverValue = !isLeftHalf ? itemValue - step : itemValue;\n } else {\n hoverValue = isLeftHalf ? itemValue - step : itemValue;\n }\n\n store.setState(\"hoveredValue\", hoverValue);\n }\n },\n [isDisabled, isReadOnly, step, itemValue, store, context.dir, propsRef],\n );\n\n const dataState: DataState = isFilled\n ? \"full\"\n : isPartiallyFilled\n ? \"partial\"\n : \"empty\";\n\n const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n return (\n \n {typeof children === \"function\"\n ? children(dataState)\n : (children ?? )}\n \n );\n}\n\nexport { Rating, RatingItem, useStore as useRating };\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": "" } ] }