{"name":"rating","title":"Rating","description":"Star rating component with hover preview.","type":"registry:ui","docs":"/components/rating","categories":["forms","data-entry"],"files":[{"path":"components/ui/rating.tsx","target":"components/ui/rating.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nconst sizeClasses = {\n  sm: \"size-4\",\n  default: \"size-5\",\n  lg: \"size-6\",\n};\n\nexport type RatingProps = {\n  value?: number;\n  onChange?: (value: number) => void;\n  max?: number;\n  disabled?: boolean;\n  readOnly?: boolean;\n  size?: \"sm\" | \"default\" | \"lg\";\n  allowHalf?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n  \"aria-invalid\"?: boolean;\n};\n\nconst StarIcon = ({\n  isFilled,\n  isHalfFilled,\n  size,\n  className,\n}: {\n  isFilled: boolean;\n  isHalfFilled: boolean;\n  size: string;\n  className?: string;\n}) => {\n  return (\n    <div className={cn(\"inline-block relative\", size, className)}>\n      {/* Empty star background */}\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className={cn(\"absolute inset-0 text-muted-foreground\")}\n      >\n        <polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\" />\n      </svg>\n      {/* Filled/Half-filled star overlay */}\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className={cn(\n          \"absolute inset-0 transition-opacity\",\n          isFilled || isHalfFilled\n            ? \"text-yellow-400 fill-yellow-400\"\n            : \"opacity-0\",\n        )}\n        style={{\n          clipPath: isHalfFilled\n            ? \"polygon(0 0, 50% 0, 50% 100%, 0 100%)\"\n            : undefined,\n        }}\n      >\n        <polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\" />\n      </svg>\n    </div>\n  );\n};\n\nfunction Rating({\n  value = 0,\n  onChange,\n  max = 5,\n  disabled = false,\n  readOnly = false,\n  allowHalf = false,\n  size = \"default\",\n  className,\n  \"aria-label\": ariaLabel = \"Rating\",\n  \"aria-invalid\": ariaInvalid,\n  ...props\n}: RatingProps) {\n  const [hoverValue, setHoverValue] = React.useState<number | null>(null);\n  const displayValue = hoverValue ?? value;\n\n  const handleMouseMove = (\n    e: React.MouseEvent<HTMLButtonElement>,\n    index: number,\n  ) => {\n    if (disabled || readOnly) return;\n\n    if (allowHalf) {\n      const { left, width } = e.currentTarget.getBoundingClientRect();\n      const percent = (e.clientX - left) / width;\n      const newValue = index + (percent > 0.5 ? 1 : 0.5);\n      setHoverValue(newValue);\n    } else {\n      setHoverValue(index + 1);\n    }\n  };\n\n  const handleClick = (rating: number) => {\n    if (disabled || readOnly) return;\n    onChange?.(rating);\n  };\n\n  const handleMouseLeave = () => {\n    setHoverValue(null);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent, rating: number) => {\n    if (disabled || readOnly) return;\n    if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault();\n      onChange?.(rating);\n    }\n  };\n\n  // Clean up rating logic for rendering\n  const renderStars = () => {\n    return Array.from({ length: max }, (_, index) => {\n      const starValue = index + 1;\n      const isFilled = displayValue >= starValue;\n      const isHalfFilled =\n        allowHalf && !isFilled && displayValue >= starValue - 0.5;\n\n      return (\n        <button\n          key={index}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={\n            value === starValue || (allowHalf && value === starValue - 0.5)\n          }\n          aria-label={`${starValue} star${starValue !== 1 ? \"s\" : \"\"}`}\n          disabled={disabled}\n          tabIndex={readOnly ? -1 : 0}\n          onClick={() => handleClick(hoverValue ?? starValue)}\n          onMouseMove={(e) => handleMouseMove(e, index)}\n          onKeyDown={(e) => handleKeyDown(e, starValue)}\n          className={cn(\n            \"p-0.5 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 transition-transform\",\n            !disabled && !readOnly && \"cursor-pointer hover:scale-110\",\n            disabled && \"cursor-not-allowed opacity-50\",\n          )}\n        >\n          <StarIcon\n            isFilled={isFilled}\n            isHalfFilled={isHalfFilled}\n            size={sizeClasses[size]}\n            className={cn(\"transition-colors\")}\n          />\n        </button>\n      );\n    });\n  };\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      aria-invalid={ariaInvalid}\n      className={cn(\"inline-flex items-center gap-0.5\", className)}\n      onMouseLeave={handleMouseLeave}\n      {...props}\n    >\n      {renderStars()}\n    </div>\n  );\n}\n\nexport { Rating };\n"}]}