{
  "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\";\n\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<typeof Rating>;\ntype ItemElement = React.ComponentRef<typeof RatingItem>;\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<string, FocusIntent> = {\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<ItemElement>,\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<ItemElement | null>[],\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: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(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<T>(\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<ItemElement | null>;\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<RatingContextValue | null>(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<FocusContextValue | null>(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<StoreState>(() => ({\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<Store>(() => {\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<RootElement | null>(\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<string | null>(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<Map<string, ItemData>>(new Map());\n\n  const autoIndexMapRef = React.useRef(new Map<string, number>());\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<HTMLDivElement>) => {\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<HTMLDivElement>) => {\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<HTMLDivElement>) => {\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<RatingContextValue>(\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<FocusContextValue>(\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    <StoreContext.Provider value={store}>\n      <RatingContext.Provider value={contextValue}>\n        <FocusContext.Provider value={focusContextValue}>\n          <RootPrimitive\n            id={rootId}\n            role=\"radiogroup\"\n            aria-orientation={orientation}\n            data-disabled={disabled ? \"\" : undefined}\n            data-readonly={readOnly ? \"\" : undefined}\n            data-orientation={orientation}\n            data-slot=\"rating\"\n            dir={dir}\n            tabIndex={isTabbingBackOut || focusableItemCount === 0 ? -1 : 0}\n            {...rootProps}\n            ref={composedRef}\n            className={cn(\n              \"flex gap-1 text-primary outline-none\",\n              orientation === \"horizontal\"\n                ? \"flex-row items-center\"\n                : \"flex-col items-start\",\n              className,\n            )}\n            onBlur={onBlur}\n            onFocus={onFocus}\n            onMouseDown={onMouseDown}\n          />\n          <svg width=\"0\" height=\"0\" style={{ position: \"absolute\" }}>\n            <defs>\n              <linearGradient id={getPartialFillGradientId(rootId, step)}>\n                {dir === \"rtl\" ? (\n                  <>\n                    <stop offset=\"50%\" stopColor=\"transparent\" />\n                    <stop offset=\"50%\" stopColor=\"currentColor\" />\n                  </>\n                ) : (\n                  <>\n                    <stop offset=\"50%\" stopColor=\"currentColor\" />\n                    <stop offset=\"50%\" stopColor=\"transparent\" />\n                  </>\n                )}\n              </linearGradient>\n            </defs>\n          </svg>\n          {isFormControl && (\n            <VisuallyHiddenInput\n              type=\"hidden\"\n              control={formTrigger}\n              name={name}\n              value={value}\n              disabled={disabled}\n              readOnly={readOnly}\n              required={required}\n            />\n          )}\n        </FocusContext.Provider>\n      </RatingContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface RatingItemProps extends Omit<\n  React.ComponentProps<\"button\">,\n  \"children\"\n> {\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<ItemElement>(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<ItemElement>) => {\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<ItemElement>) => {\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<ItemElement>) => {\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<ItemElement>) => {\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<ItemElement>) => {\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<ItemElement>) => {\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<ItemElement>) => {\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    <ItemPrimitive\n      role=\"radio\"\n      type=\"button\"\n      id={itemId}\n      aria-checked={isFilled}\n      aria-posinset={itemValue}\n      aria-setsize={context.max}\n      data-disabled={isDisabled ? \"\" : undefined}\n      data-readonly={isReadOnly ? \"\" : undefined}\n      data-state={isFilled ? \"full\" : isPartiallyFilled ? \"partial\" : \"empty\"}\n      data-hovered={isHovered ? \"\" : undefined}\n      data-slot=\"rating-item\"\n      disabled={isDisabled}\n      tabIndex={isTabStop ? 0 : -1}\n      {...itemProps}\n      ref={composedRef}\n      style={{\n        ...itemProps.style,\n        ...(isPartiallyFilled && {\n          \"--partial-fill\": `url(#${getPartialFillGradientId(context.rootId, step)})`,\n        }),\n      }}\n      className={cn(\n        \"inline-flex items-center justify-center rounded transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n        \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:transition-colors [&_svg]:duration-200 data-[state=empty]:[&_svg]:fill-transparent data-[state=full]:[&_svg]:fill-current data-[state=partial]:[&_svg]:fill-(--partial-fill) [&_svg:not([class*='size-'])]:size-full\",\n        context.size === \"sm\"\n          ? \"size-4\"\n          : context.size === \"lg\"\n            ? \"size-6\"\n            : \"size-5\",\n        className,\n      )}\n      onClick={onClick}\n      onFocus={onFocus}\n      onKeyDown={onKeyDown}\n      onMouseDown={onMouseDown}\n      onMouseEnter={onMouseEnter}\n      onMouseMove={onMouseMove}\n      onMouseLeave={onMouseLeave}\n    >\n      {typeof children === \"function\"\n        ? children(dataState)\n        : (children ?? <Star />)}\n    </ItemPrimitive>\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<T = InputValue> extends Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"checked\" | \"onReset\"\n> {\n  value?: T;\n  checked?: boolean;\n  control: HTMLElement | null;\n  bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput<T = InputValue>(\n  props: VisuallyHiddenInputProps<T>,\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<HTMLInputElement>(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<React.CSSProperties>(() => {\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    <input\n      type={type}\n      {...inputProps}\n      ref={inputRef}\n      aria-hidden={isCheckInput}\n      tabIndex={-1}\n      defaultChecked={isCheckInput ? checked : undefined}\n      style={composedStyle}\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<T> = React.Ref<T> | 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<T>(ref: PossibleRef<T>, 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<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\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<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}