{
  "name": "star-rating-fractions",
  "type": "registry:component",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Star } from \"lucide-react\";\nimport * as React from \"react\";\n\n// Add ID generator outside component to maintain counter\nlet nextId = 0;\nconst generateStarIds = (count: number) =>\n  Array.from({ length: count }, () => `star-${nextId++}`);\n\ninterface StarRatingBasicProps {\n  value: number;\n  onChange?: (value: number) => void;\n  className?: string;\n  iconSize?: number;\n  maxStars?: number;\n  readOnly?: boolean;\n  color?: string;\n}\n\nconst StarIcon = React.memo(\n  ({\n    iconSize,\n    index,\n    isInteractive,\n    onClick,\n    onMouseMove,\n    style,\n  }: {\n    index: number;\n    style: React.CSSProperties;\n    iconSize: number;\n    onClick: (e: React.MouseEvent<SVGElement>) => void;\n    onMouseMove: (e: React.MouseEvent<SVGElement>) => void;\n    isInteractive: boolean;\n  }) => (\n    <Star\n      key={index}\n      size={iconSize}\n      fill={style.fill}\n      color={style.color}\n      onClick={onClick}\n      onMouseMove={onMouseMove}\n      className={cn(\n        \"transition-colors duration-200\",\n        isInteractive && \"cursor-pointer\"\n      )}\n      style={style}\n    />\n  )\n);\nStarIcon.displayName = \"StarIcon\";\n\nconst StarRating_Fractions = ({\n  className,\n  color = \"#e4c616\",\n  iconSize = 24,\n  maxStars = 5,\n  onChange,\n  readOnly = false,\n  value,\n}: StarRatingBasicProps) => {\n  const [hoverRating, setHoverRating] = React.useState<number | null>(null);\n  // Generate stable IDs on component mount\n  const [starIds] = React.useState(() => generateStarIds(maxStars));\n\n  const calculateRating = React.useCallback(\n    (index: number, event: React.MouseEvent<SVGElement>) => {\n      const star = event.currentTarget;\n      const rect = star.getBoundingClientRect();\n      const x = event.clientX - rect.left;\n      const width = rect.width;\n      const clickPosition = x / width;\n\n      let fraction = 1;\n      if (clickPosition <= 0.25) fraction = 0.25;\n      else if (clickPosition <= 0.5) fraction = 0.5;\n      else if (clickPosition <= 0.75) fraction = 0.75;\n\n      return index + fraction;\n    },\n    []\n  );\n\n  const handleStarClick = React.useCallback(\n    (index: number, event: React.MouseEvent<SVGElement>) => {\n      if (readOnly || !onChange) return;\n      const newRating = calculateRating(index, event);\n      onChange(newRating);\n    },\n    [readOnly, onChange, calculateRating]\n  );\n\n  const handleStarHover = React.useCallback(\n    (index: number, event: React.MouseEvent<SVGElement>) => {\n      if (!readOnly) {\n        const previewRating = calculateRating(index, event);\n        setHoverRating(previewRating);\n      }\n    },\n    [readOnly, calculateRating]\n  );\n\n  const handleMouseLeave = React.useCallback(() => {\n    if (!readOnly) {\n      setHoverRating(null);\n    }\n  }, [readOnly]);\n\n  const getStarStyle = React.useCallback(\n    (index: number) => {\n      const ratingToUse =\n        !readOnly && hoverRating !== null ? hoverRating : value;\n      const difference = ratingToUse - index;\n\n      if (difference <= 0) return { color: \"gray\", fill: \"transparent\" };\n      if (difference >= 1) return { color: color, fill: color };\n\n      return {\n        color: color,\n        fill: `url(#${starIds[index]})`,\n      } as React.CSSProperties;\n    },\n    [readOnly, hoverRating, value, color, starIds]\n  );\n\n  const renderGradientDefs = () => {\n    const ratingToUse = !readOnly && hoverRating !== null ? hoverRating : value;\n    const partialStarIndex = Math.floor(ratingToUse);\n    const partialFill = (ratingToUse % 1) * 100;\n\n    // Only create gradient for partial star\n    if (partialFill > 0) {\n      return (\n        <linearGradient\n          id={starIds[partialStarIndex]}\n          x1=\"0%\"\n          y1=\"0%\"\n          x2=\"100%\"\n          y2=\"0%\"\n        >\n          <stop offset={`${partialFill}%`} stopColor={color} />\n          <stop offset={`${partialFill}%`} stopColor=\"transparent\" />\n        </linearGradient>\n      );\n    }\n    return null;\n  };\n\n  const renderStars = () => {\n    return Array.from({ length: maxStars }).map((_, index) => {\n      const style = getStarStyle(index);\n      return (\n        <StarIcon\n          key={starIds[index]}\n          index={index}\n          style={style}\n          iconSize={iconSize}\n          onClick={(e) => handleStarClick(index, e)}\n          onMouseMove={(e) => handleStarHover(index, e)}\n          isInteractive={!readOnly}\n        />\n      );\n    });\n  };\n\n  return (\n    <div\n      className={cn(\"relative flex items-center gap-x-0.5\", className)}\n      onMouseLeave={handleMouseLeave}\n    >\n      <svg width=\"0\" height=\"0\" style={{ position: \"absolute\" }}>\n        <defs>{renderGradientDefs()}</defs>\n      </svg>\n      {renderStars()}\n    </div>\n  );\n};\n\nexport default StarRating_Fractions;\n",
      "path": "/components/commerce-ui/components/star-rating/fractions/star-rating-fractions.tsx",
      "target": "components/commerce-ui/star-rating-fractions.tsx"
    }
  ]
}