{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating",
  "type": "registry:ui",
  "description": "Star rating component with half-values, multiple icons, and keyboard navigation",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/rating.tsx",
      "content": "import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { Star, Heart, Circle } from 'lucide-react'\n\nconst ratingVariants = cva('flex items-center gap-0.5', {\n  variants: {\n    size: {\n      sm: '[&_svg]:h-4 [&_svg]:w-4',\n      md: '[&_svg]:h-5 [&_svg]:w-5',\n      lg: '[&_svg]:h-6 [&_svg]:w-6',\n      xl: '[&_svg]:h-8 [&_svg]:w-8',\n    },\n  },\n  defaultVariants: {\n    size: 'md',\n  },\n})\n\nconst iconMap = {\n  star: Star,\n  heart: Heart,\n  circle: Circle,\n}\n\nexport interface RatingProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>,\n    VariantProps<typeof ratingVariants> {\n  value?: number\n  defaultValue?: number\n  max?: number\n  precision?: 0.5 | 1\n  icon?: 'star' | 'heart' | 'circle'\n  readOnly?: boolean\n  disabled?: boolean\n  onChange?: (value: number) => void\n  onHoverChange?: (value: number | null) => void\n}\n\nconst Rating = React.forwardRef<HTMLDivElement, RatingProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = 0,\n      max = 5,\n      precision = 1,\n      icon = 'star',\n      size,\n      readOnly = false,\n      disabled = false,\n      onChange,\n      onHoverChange,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n    const [hoverValue, setHoverValue] = React.useState<number | null>(null)\n\n    const isControlled = controlledValue !== undefined\n    const currentValue = isControlled ? controlledValue : uncontrolledValue\n    const displayValue = hoverValue ?? currentValue\n\n    const Icon = iconMap[icon]\n\n    const handleClick = (index: number, isHalf: boolean) => {\n      if (readOnly || disabled) return\n\n      const newValue = isHalf && precision === 0.5 ? index + 0.5 : index + 1\n\n      if (!isControlled) {\n        setUncontrolledValue(newValue)\n      }\n      onChange?.(newValue)\n    }\n\n    const handleMouseMove = (\n      e: React.MouseEvent<HTMLButtonElement>,\n      index: number\n    ) => {\n      if (readOnly || disabled) return\n\n      const rect = e.currentTarget.getBoundingClientRect()\n      const isHalf = e.clientX - rect.left < rect.width / 2\n\n      const newHoverValue =\n        isHalf && precision === 0.5 ? index + 0.5 : index + 1\n      setHoverValue(newHoverValue)\n      onHoverChange?.(newHoverValue)\n    }\n\n    const handleMouseLeave = () => {\n      if (readOnly || disabled) return\n      setHoverValue(null)\n      onHoverChange?.(null)\n    }\n\n    const handleKeyDown = (e: React.KeyboardEvent) => {\n      if (readOnly || disabled) return\n\n      let newValue = currentValue\n\n      switch (e.key) {\n        case 'ArrowRight':\n        case 'ArrowUp':\n          e.preventDefault()\n          newValue = Math.min(currentValue + precision, max)\n          break\n        case 'ArrowLeft':\n        case 'ArrowDown':\n          e.preventDefault()\n          newValue = Math.max(currentValue - precision, 0)\n          break\n        case 'Home':\n          e.preventDefault()\n          newValue = 0\n          break\n        case 'End':\n          e.preventDefault()\n          newValue = max\n          break\n        default:\n          return\n      }\n\n      if (!isControlled) {\n        setUncontrolledValue(newValue)\n      }\n      onChange?.(newValue)\n    }\n\n    return (\n      <div\n        ref={ref}\n        role=\"slider\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={currentValue}\n        aria-label=\"Rating\"\n        tabIndex={readOnly || disabled ? -1 : 0}\n        onKeyDown={handleKeyDown}\n        onMouseLeave={handleMouseLeave}\n        className={cn(\n          ratingVariants({ size }),\n          disabled && 'opacity-50 pointer-events-none',\n          !readOnly && !disabled && 'cursor-pointer',\n          className\n        )}\n        {...props}\n      >\n        {Array.from({ length: max }, (_, index) => {\n          const fillValue = displayValue - index\n          const isFilled = fillValue >= 1\n          const isHalfFilled = fillValue > 0 && fillValue < 1\n\n          return (\n            <button\n              key={index}\n              type=\"button\"\n              tabIndex={-1}\n              disabled={disabled || readOnly}\n              onClick={(e) => {\n                const rect = e.currentTarget.getBoundingClientRect()\n                const isHalf = e.clientX - rect.left < rect.width / 2\n                handleClick(index, isHalf)\n              }}\n              onMouseMove={(e) => handleMouseMove(e, index)}\n              className={cn(\n                'relative transition-transform duration-150 focus:outline-none',\n                !readOnly && !disabled && 'hover:scale-110'\n              )}\n            >\n              {/* Empty icon (background) */}\n              <Icon\n                className={cn(\n                  'stroke-foreground stroke-[2px] fill-muted transition-colors duration-150'\n                )}\n              />\n\n              {/* Filled icon (overlay) */}\n              {(isFilled || isHalfFilled) && (\n                <Icon\n                  className={cn(\n                    'absolute inset-0 stroke-foreground stroke-[2px] fill-primary transition-colors duration-150'\n                  )}\n                  style={\n                    isHalfFilled\n                      ? {\n                          clipPath: 'polygon(0 0, 50% 0, 50% 100%, 0 100%)',\n                        }\n                      : undefined\n                  }\n                />\n              )}\n            </button>\n          )\n        })}\n      </div>\n    )\n  }\n)\nRating.displayName = 'Rating'\n\nexport { Rating, ratingVariants }\n",
      "type": "registry:ui",
      "target": "components/ui/rating.tsx"
    }
  ]
}