{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"rating","type":"registry:ui","title":"rating","description":"A star rating component with keyboard navigation and hover effects.","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":["@radix-ui/react-use-controllable-state","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { type LucideProps, StarIcon } from \"lucide-react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype RatingContextValue = {\n  value: number;\n  readOnly: boolean;\n  hoverValue: number | null;\n  focusedStar: number | null;\n  handleValueChange: (\n    event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,\n    value: number\n  ) => void;\n  handleKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;\n  setHoverValue: (value: number | null) => void;\n  setFocusedStar: (value: number | null) => void;\n};\n\nconst RatingContext = createContext<RatingContextValue | null>(null);\n\nconst useRating = () => {\n  const context = useContext(RatingContext);\n  if (!context) {\n    throw new Error(\"useRating must be used within a Rating component\");\n  }\n  return context;\n};\n\nexport type RatingButtonProps = LucideProps & {\n  index?: number;\n  icon?: ReactElement<LucideProps>;\n};\n\nexport const RatingButton = ({\n  index: providedIndex,\n  size = 20,\n  className,\n  icon = <StarIcon />,\n}: RatingButtonProps) => {\n  const {\n    value,\n    readOnly,\n    hoverValue,\n    focusedStar,\n    handleValueChange,\n    handleKeyDown,\n    setHoverValue,\n    setFocusedStar,\n  } = useRating();\n\n  const index = providedIndex ?? 0;\n  const isActive = index < (hoverValue ?? focusedStar ?? value ?? 0);\n  let tabIndex = -1;\n\n  if (!readOnly) {\n    tabIndex = value === index + 1 ? 0 : -1;\n  }\n\n  const handleClick = useCallback(\n    (event: MouseEvent<HTMLButtonElement>) => {\n      handleValueChange(event, index + 1);\n    },\n    [handleValueChange, index]\n  );\n\n  const handleMouseEnter = useCallback(() => {\n    if (!readOnly) {\n      setHoverValue(index + 1);\n    }\n  }, [readOnly, setHoverValue, index]);\n\n  const handleFocus = useCallback(() => {\n    setFocusedStar(index + 1);\n  }, [setFocusedStar, index]);\n\n  const handleBlur = useCallback(() => {\n    setFocusedStar(null);\n  }, [setFocusedStar]);\n\n  return (\n    <button\n      className={cn(\n        \"rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"p-0.5\",\n        readOnly && \"cursor-default\",\n        className\n      )}\n      disabled={readOnly}\n      onBlur={handleBlur}\n      onClick={handleClick}\n      onFocus={handleFocus}\n      onKeyDown={handleKeyDown}\n      onMouseEnter={handleMouseEnter}\n      tabIndex={tabIndex}\n      type=\"button\"\n    >\n      {cloneElement(icon, {\n        size,\n        className: cn(\n          \"transition-colors duration-200\",\n          isActive && \"fill-current\",\n          !readOnly && \"cursor-pointer\"\n        ),\n        \"aria-hidden\": \"true\",\n      })}\n    </button>\n  );\n};\n\nexport type RatingProps = {\n  defaultValue?: number;\n  value?: number;\n  onChange?: (\n    event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,\n    value: number\n  ) => void;\n  onValueChange?: (value: number) => void;\n  readOnly?: boolean;\n  className?: string;\n  children?: ReactNode;\n};\n\nexport const Rating = ({\n  value: controlledValue,\n  onValueChange: controlledOnValueChange,\n  defaultValue = 0,\n  onChange,\n  readOnly = false,\n  className,\n  children,\n  ...props\n}: RatingProps) => {\n  const [hoverValue, setHoverValue] = useState<number | null>(null);\n  const [focusedStar, setFocusedStar] = useState<number | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [value, onValueChange] = useControllableState({\n    defaultProp: defaultValue,\n    prop: controlledValue,\n    onChange: controlledOnValueChange,\n  });\n\n  const handleValueChange = useCallback(\n    (\n      event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,\n      newValue: number\n    ) => {\n      if (!readOnly) {\n        onChange?.(event, newValue);\n        onValueChange?.(newValue);\n      }\n    },\n    [readOnly, onChange, onValueChange]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (readOnly) {\n        return;\n      }\n\n      const total = Children.count(children);\n      let newValue = focusedStar !== null ? focusedStar : (value ?? 0);\n\n      switch (event.key) {\n        case \"ArrowRight\":\n          if (event.shiftKey || event.metaKey) {\n            newValue = total;\n          } else {\n            newValue = Math.min(total, newValue + 1);\n          }\n          break;\n        case \"ArrowLeft\":\n          if (event.shiftKey || event.metaKey) {\n            newValue = 1;\n          } else {\n            newValue = Math.max(1, newValue - 1);\n          }\n          break;\n        default:\n          return;\n      }\n\n      event.preventDefault();\n      setFocusedStar(newValue);\n      handleValueChange(event, newValue);\n    },\n    [focusedStar, value, children, readOnly, handleValueChange]\n  );\n\n  useEffect(() => {\n    if (focusedStar !== null && containerRef.current) {\n      const buttons = containerRef.current.querySelectorAll(\"button\");\n      buttons[focusedStar - 1]?.focus();\n    }\n  }, [focusedStar]);\n\n  const contextValue: RatingContextValue = {\n    value: value ?? 0,\n    readOnly,\n    hoverValue,\n    focusedStar,\n    handleValueChange,\n    handleKeyDown,\n    setHoverValue,\n    setFocusedStar,\n  };\n\n  return (\n    <RatingContext.Provider value={contextValue}>\n      <div\n        aria-label=\"Rating\"\n        className={cn(\"inline-flex items-center gap-0.5\", className)}\n        onMouseLeave={() => setHoverValue(null)}\n        ref={containerRef}\n        role=\"radiogroup\"\n        {...props}\n      >\n        {Children.map(children, (child, index) => {\n          if (!child) {\n            return null;\n          }\n\n          return cloneElement(child as ReactElement<RatingButtonProps>, {\n            index,\n          });\n        })}\n      </div>\n    </RatingContext.Provider>\n  );\n};\n","target":"components/kibo-ui/rating/index.tsx"}],"css":{}}