{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating",
  "title": "Rating",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A native radio-group star rating with keyboard arrows, value text, and a clear action.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/rating.tsx",
      "content": "// Ballmac UI: Rating. https://ui.ballmac.com/components/rating\n\"use client\"\n\nimport * as React from \"react\"\nimport { Star } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype RatingProps = Omit<\n  React.ComponentProps<\"fieldset\">,\n  \"onChange\" | \"defaultValue\"\n> & {\n  /** Controlled rating. Zero means unrated. */\n  value?: number\n  /** Initial rating when uncontrolled. */\n  defaultValue?: number\n  /** Called when the rating changes. */\n  onValueChange?: (value: number) => void\n  /** Highest rating. */\n  max?: number\n  /** Visible and accessible label. */\n  label?: string\n  /** Native radio group name. */\n  name?: string\n  /** Show the current value beside the stars. */\n  showValue?: boolean\n  /** Disable changes while keeping the rating visible. */\n  readOnly?: boolean\n}\nfunction Rating({\n  value,\n  defaultValue = 0,\n  onValueChange,\n  max = 5,\n  label,\n  name,\n  showValue = true,\n  readOnly = false,\n  disabled = false,\n  className,\n  ...props\n}: RatingProps) {\n  const msg = useMessages()\n  label ??= msg(\"rating.label\", \"Rating\")\n  const [internal, setInternal] = React.useState(defaultValue)\n  const [hovered, setHovered] = React.useState(0)\n  const current = value ?? internal\n  const groupName = name ?? React.useId()\n  const count = Math.max(1, Math.min(10, Math.floor(max)))\n  function commit(next: number) {\n    if (value === undefined) setInternal(next)\n    onValueChange?.(next)\n  }\n  return (\n    <fieldset\n      data-slot=\"rating\"\n      disabled={disabled}\n      className={cn(\"min-w-0\", className)}\n      {...props}\n    >\n      <legend className=\"mb-1.5 text-sm font-medium\">{label}</legend>\n      <div\n        className=\"flex flex-wrap items-center gap-1\"\n        onMouseLeave={() => setHovered(0)}\n      >\n        {Array.from({ length: count }, (_, index) => index + 1).map(\n          (number) => (\n            <label\n              key={number}\n              data-slot=\"rating-item\"\n              className={cn(\n                \"relative flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 motion-reduce:transition-none\",\n                !readOnly && !disabled && \"cursor-pointer hover:bg-accent\",\n                (hovered || current) >= number && \"text-chart-3\",\n                readOnly && \"pointer-events-none\",\n              )}\n              onMouseEnter={() => setHovered(readOnly ? 0 : number)}\n            >\n              <input\n                type=\"radio\"\n                name={groupName}\n                value={number}\n                checked={current === number}\n                disabled={disabled || readOnly}\n                aria-label={msg(\"rating.ofStars\", \"{number} of {count} stars\", { number, count })}\n                onChange={() => {\n                  if (!readOnly) commit(number)\n                }}\n                className=\"peer sr-only\"\n              />\n              <Star\n                aria-hidden=\"true\"\n                className={cn(\n                  \"size-5 peer-focus-visible:drop-shadow-md\",\n                  (hovered || current) >= number && \"fill-current\",\n                )}\n              />\n              <span\n                aria-hidden=\"true\"\n                className=\"pointer-events-none absolute inset-0 rounded-md peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50\"\n              />\n            </label>\n          ),\n        )}\n        {showValue && (\n          <span\n            data-slot=\"rating-value\"\n            className=\"ms-2 text-sm text-muted-foreground tabular-nums\"\n          >\n            {current ? `${current} / ${count}` : \"Not rated\"}\n          </span>\n        )}\n        {!readOnly && current > 0 && (\n          <button\n            type=\"button\"\n            disabled={disabled}\n            onClick={() => commit(0)}\n            className=\"ms-1 rounded-md px-2 py-1 text-xs text-muted-foreground outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50\"\n          >\n            {msg(\"rating.clear\", \"Clear\")}\n          </button>\n        )}\n      </div>\n    </fieldset>\n  )\n}\nexport { Rating, type RatingProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/rating.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "rating",
      "stars",
      "feedback"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "A native radio-group star rating with keyboard arrows, value text, and a clear action.",
      "whenToUse": [
        "Collect a quick score or satisfaction rating",
        "Display a read-only score beside content"
      ],
      "whenNotToUse": [
        "Use a text field for detailed feedback"
      ],
      "composesWith": [
        "button"
      ],
      "a11y": [
        {
          "keys": "Arrow Left / Right",
          "action": "Changes the selected native radio"
        },
        {
          "keys": "Tab / Space",
          "action": "Focuses and activates Clear"
        }
      ],
      "customization": [
        "max, showValue, readOnly",
        "value / defaultValue and onValueChange"
      ]
    },
    "examples": [
      "rating-demo",
      "rating-states"
    ],
    "url": "https://ui.ballmac.com/components/rating"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}