{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating",
  "title": "Rating",
  "description": "Pick a score with stars or any icon, in whole or half steps, from the keyboard or pointer.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/rating.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { StarIcon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { cn } from \"@/registry/default/lib/utils\"\n\nfunction Rating({\n  value,\n  defaultValue = 0,\n  onValueChange,\n  max = 5,\n  step = 1,\n  icon = <StarIcon />,\n  readOnly = false,\n  disabled = false,\n  className,\n  \"aria-label\": ariaLabel,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  max?: number\n  step?: 1 | 0.5\n  icon?: React.ReactNode\n  readOnly?: boolean\n  disabled?: boolean\n}) {\n  const { value: rating, setValue } = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  })\n  const [hovered, setHovered] = React.useState<number | null>(null)\n  const options = Array.from(\n    { length: Math.round(max / step) },\n    (_, index) => (index + 1) * step\n  )\n  const shown = hovered ?? rating\n  const tabStop = options.includes(rating) ? rating : options[0]\n\n  function choose(option: number, from: HTMLElement) {\n    setValue(option)\n    from\n      .closest('[data-slot=\"rating\"]')\n      ?.querySelector<HTMLElement>(`[data-value=\"${option}\"]`)\n      ?.focus()\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {\n    const index = options.indexOf(rating)\n    const from = event.currentTarget\n    switch (event.key) {\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        choose(options[Math.min(index + 1, options.length - 1)], from)\n        break\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        choose(options[Math.max(index - 1, 0)], from)\n        break\n      case \"Home\":\n        choose(options[0], from)\n        break\n      case \"End\":\n        choose(options.at(-1)!, from)\n        break\n      case \" \":\n      case \"Enter\":\n        choose(Number(event.currentTarget.dataset.value), from)\n        break\n      default:\n        return\n    }\n    setHovered(null)\n    event.preventDefault()\n  }\n\n  return (\n    <div\n      role={readOnly ? \"img\" : \"radiogroup\"}\n      data-slot=\"rating\"\n      aria-label={\n        readOnly\n          ? [ariaLabel, `${rating} out of ${max}`].filter(Boolean).join(\": \")\n          : ariaLabel\n      }\n      aria-disabled={disabled || undefined}\n      onPointerLeave={() => setHovered(null)}\n      className={cn(\n        \"inline-flex items-center gap-0.5 text-primary\",\n        \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {Array.from({ length: max }, (_, index) => {\n        const fill = Math.min(Math.max(shown - index, 0), 1)\n\n        return (\n          <span\n            key={index}\n            data-slot=\"rating-item\"\n            className=\"relative inline-flex [&_svg]:size-5 [&_svg]:shrink-0\"\n          >\n            <span aria-hidden className=\"inline-flex text-muted-foreground/40\">\n              {icon}\n            </span>\n            <span\n              aria-hidden\n              className=\"absolute inset-y-0 left-0 inline-flex overflow-hidden [&_svg]:fill-current\"\n              style={{ width: `${fill * 100}%` }}\n            >\n              {icon}\n            </span>\n            {!readOnly &&\n              options\n                .filter((option) => Math.ceil(option) === index + 1)\n                .map((option) => (\n                  <span\n                    key={option}\n                    role=\"radio\"\n                    data-value={option}\n                    aria-checked={option === rating}\n                    aria-label={`${option} out of ${max}`}\n                    tabIndex={!disabled && option === tabStop ? 0 : -1}\n                    onClick={(event) => choose(option, event.currentTarget)}\n                    onKeyDown={handleKeyDown}\n                    onPointerEnter={() => setHovered(option)}\n                    className={cn(\n                      \"absolute inset-y-0 rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n                      step === 1\n                        ? \"inset-x-0\"\n                        : option % 1\n                          ? \"left-0 w-1/2\"\n                          : \"right-0 w-1/2\"\n                    )}\n                  />\n                ))}\n          </span>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { Rating }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}