{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating",
  "title": "Rating",
  "description": "The nx-ui Rating component.",
  "registryDependencies": [
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/ui/rating.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Rating — the Untitled UI star rating, in display and interactive modes.\n *\n * Display mode renders fractional stars via a clip-path progress fill (ported\n * from the vendored `foundations/rating-stars.tsx`). Interactive mode lets the\n * user pick a whole-star rating with hover preview and keyboard support. The\n * filled star uses the Nexus accent (`--color-utility-yellow-400`); the empty\n * track uses `bg-tertiary` so it flips in dark mode.\n */\n\nconst STAR_PATH =\n  \"M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z\"\n\n/** Fill percentage (0–100) for a star at `starIndex` given the rating. */\nfunction starProgress(starIndex: number, rating: number, maxRating: number) {\n  const clamped = Math.min(Math.max(rating, 0), maxRating)\n  const diff = clamped - starIndex\n  if (diff >= 1) return 100\n  if (diff <= 0) return 0\n  return Math.round(diff * 100)\n}\n\nconst sizeClass = {\n  sm: \"size-4\",\n  md: \"size-5\",\n  lg: \"size-6\",\n} as const\n\nfunction StarIcon({\n  progress = 100,\n  className,\n}: {\n  progress?: number\n  className?: string\n}) {\n  const id = React.useId()\n  return (\n    <svg\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={cn(\"text-utility-yellow-400\", className)}\n    >\n      <path d={STAR_PATH} className=\"fill-bg-tertiary\" />\n      <g clipPath={`url(#clip-${id})`}>\n        <path d={STAR_PATH} fill=\"currentColor\" />\n      </g>\n      <defs>\n        <clipPath id={`clip-${id}`}>\n          <rect width={`${progress}%`} height=\"20\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  )\n}\n\nfunction Rating({\n  value = 0,\n  defaultValue,\n  onValueChange,\n  max = 5,\n  size = \"md\",\n  readOnly = false,\n  disabled = false,\n  className,\n  \"aria-label\": ariaLabel = \"Rating\",\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\" | \"defaultValue\"> & {\n  /** Current rating. In interactive mode this is the committed value. */\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  max?: number\n  size?: keyof typeof sizeClass\n  /** Display-only: renders fractional stars, no interaction. */\n  readOnly?: boolean\n  disabled?: boolean\n}) {\n  const isControlled = onValueChange !== undefined && defaultValue === undefined\n  const [internal, setInternal] = React.useState(defaultValue ?? value)\n  const current = isControlled ? value : internal\n  const [hover, setHover] = React.useState<number | null>(null)\n\n  const interactive = !readOnly && !disabled\n  const shown = hover ?? current\n\n  const set = (next: number) => {\n    if (!interactive) return\n    if (!isControlled) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  if (!interactive) {\n    return (\n      <div\n        data-slot=\"rating\"\n        role=\"img\"\n        aria-label={`${ariaLabel}: ${current} of ${max}`}\n        className={cn(\"inline-flex\", disabled && \"opacity-50\", className)}\n        {...props}\n      >\n        {Array.from({ length: max }).map((_, index) => (\n          <StarIcon\n            key={index}\n            progress={starProgress(index, current, max)}\n            className={sizeClass[size]}\n          />\n        ))}\n      </div>\n    )\n  }\n\n  return (\n    <div\n      data-slot=\"rating\"\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      className={cn(\"inline-flex\", className)}\n      onMouseLeave={() => setHover(null)}\n      {...props}\n    >\n      {Array.from({ length: max }).map((_, index) => {\n        const starValue = index + 1\n        return (\n          <button\n            key={index}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={current === starValue}\n            aria-label={`${starValue} star${starValue === 1 ? \"\" : \"s\"}`}\n            className=\"cursor-pointer rounded-sm p-0.5 outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n            onClick={() => set(starValue)}\n            onMouseEnter={() => setHover(starValue)}\n            onFocus={() => setHover(starValue)}\n            onBlur={() => setHover(null)}\n          >\n            <StarIcon\n              progress={shown >= starValue ? 100 : 0}\n              className={sizeClass[size]}\n            />\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { Rating }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}