{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"rating","type":"registry:component","title":"Rating","description":"A row of stars that previews the score on hover, fills up to the pick in a quick wave, and moves one star at a time with the arrow keys.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/rating.tsx","type":"registry:component","target":"@components/motion/rating.tsx","content":"\"use client\";\n// easeui.dev/components/motion/rating\n\nimport { Star } from \"lucide-react\";\nimport { useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type RatingSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface RatingProps {\n  /** Controlled value, from 0 to `max`. */\n  value?: number;\n  /** Starting value when uncontrolled. Default 0. */\n  defaultValue?: number;\n  /** Called with the new value each time a star is picked. */\n  onValueChange?: (value: number) => void;\n  /** How many stars. Default 5. */\n  max?: number;\n  /** Shows the value without letting anyone change it. Default false. */\n  readOnly?: boolean;\n  /** Default \"md\". */\n  size?: RatingSize;\n  /** Names the group for screen readers. Default \"Rating\". */\n  label?: string;\n  className?: string;\n}\n\nconst SIZES: Record<RatingSize, string> = { sm: \"h-4 w-4\", md: \"h-6 w-6\", lg: \"h-8 w-8\" };\n\n/**\n * A row of stars. Hovering previews the score, picking one fills the row up to it in a quick\n * wave, and the arrow keys move it one star at a time. Built on native radio inputs.\n */\nexport function Rating({\n  value,\n  defaultValue = 0,\n  onValueChange,\n  max = 5,\n  readOnly = false,\n  size = \"md\",\n  label = \"Rating\",\n  className,\n}: RatingProps) {\n  const name = useId();\n  const [inner, setInner] = useState(defaultValue);\n  const current = value ?? inner;\n  const [hover, setHover] = useState<number | null>(null);\n  // Bumped on every pick, so the pop replays even when the same star is picked twice.\n  const [wave, setWave] = useState(0);\n  const shown = hover ?? current;\n\n  const pick = (next: number) => {\n    if (readOnly) return;\n    setInner(next);\n    setWave((n) => n + 1);\n    onValueChange?.(next);\n  };\n\n  return (\n    <fieldset\n      onPointerLeave={() => setHover(null)}\n      disabled={readOnly}\n      className={cn(\"m-0 inline-flex min-w-0 items-center gap-0.5 border-0 p-0\", className)}\n    >\n      <legend className=\"sr-only\">{label}</legend>\n      {Array.from({ length: max }, (_, index) => index + 1).map((score) => {\n        const on = score <= shown;\n        return (\n          <label\n            key={score}\n            onPointerEnter={() => !readOnly && setHover(score)}\n            className={cn(\n              \"relative grid touch-manipulation place-items-center rounded-md\",\n              readOnly ? \"cursor-default\" : \"cursor-pointer\",\n              // Grows the hit area without spacing the stars apart.\n              \"after:absolute after:-inset-1\",\n            )}\n          >\n            <input\n              type=\"radio\"\n              name={name}\n              value={score}\n              checked={score === current}\n              onChange={() => pick(score)}\n              className=\"peer sr-only\"\n            />\n            <span className=\"sr-only\">\n              {score} star{score === 1 ? \"\" : \"s\"}\n            </span>\n            <span aria-hidden=\"true\" className=\"grid rounded-md peer-focus-visible:ring-2 peer-focus-visible:ring-foreground/40\">\n              <Star\n                key={score <= current ? wave : 0}\n                style={{ animationDelay: `${(score - 1) * 35}ms` }}\n                className={cn(\n                  SIZES[size],\n                  // Hover is instant, since it fires as the pointer sweeps along the row.\n                  on ? \"fill-warning text-warning\" : \"fill-transparent text-foreground/25\",\n                  wave > 0 && score <= current && \"animate-[rating-pop_320ms_cubic-bezier(0.23,1,0.32,1)_both] motion-reduce:animate-none\",\n                )}\n              />\n            </span>\n          </label>\n        );\n      })}\n      <style>{\"@keyframes rating-pop { 40% { transform: scale(1.22) } }\"}</style>\n    </fieldset>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}