{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating",
  "type": "registry:ui",
  "title": "Rating",
  "description": "Star rating input with half-star precision, keyboard arrows, and read-only display mode.",
  "dependencies": [
    "@saasflare/ui"
  ],
  "files": [
    {
      "path": "components/ui/rating.tsx",
      "type": "registry:ui",
      "content": "// @draft\n\"use client\"\n\n/**\n * @fileoverview Saasflare Rating — star rating input with half-star and\n * read-only support.\n * @author Saasflare™\n *\n * Self-contained: inline SVG star, no `react-rating` dep. Supports\n * controlled/uncontrolled, half-precision via `allowHalf`, keyboard\n * adjustment (arrow keys), and a read-only display mode for showing\n * average scores.\n *\n * @module packages/ui/components/ui/rating\n * @package ui\n * @layer core\n *\n * @example\n * const [score, setScore] = useState(0);\n * <Rating value={score} onChange={setScore} allowHalf />\n *\n * @example\n * // Display-only average score\n * <Rating value={4.3} readOnly allowHalf size=\"sm\" />\n */\n\nimport { useCallback, useId, useState, type MouseEvent } from \"react\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\n\nconst SIZE_PX = { sm: 14, md: 20, lg: 28, xl: 36 } as const\n/** Star size preset of a {@link Rating} — per-star pixel size (`sm` 14px, `md` 20px, `lg` 28px, `xl` 36px). */\nexport type RatingSize = keyof typeof SIZE_PX\n\n/** Props for the Rating component. */\nexport interface RatingProps extends SaasflareComponentProps {\n    /** Controlled value (0..count). */\n    value?: number\n    /** Uncontrolled initial value. */\n    defaultValue?: number\n    /** Called when the user clicks a star. */\n    onChange?: (value: number) => void\n    /** Number of stars. Default: `5`. */\n    count?: number\n    /** Allow half-star granularity. Default: `false`. */\n    allowHalf?: boolean\n    /** Read-only display mode (no interaction, no hover preview). */\n    readOnly?: boolean\n    /** Disable the rating. */\n    disabled?: boolean\n    /** Star size. Default: `\"md\"`. */\n    size?: RatingSize\n    /** Star color (any CSS color). Default: `var(--warning)` — brand-independent gold. */\n    color?: string\n    /** Additional class names. */\n    className?: string\n    /** Accessible label. */\n    \"aria-label\"?: string\n}\n\nfunction StarPath({\n    fillPercent,\n    color,\n    px,\n    onClick,\n    onMouseMove,\n}: {\n    fillPercent: number\n    color: string\n    px: number\n    onClick?: (e: MouseEvent<SVGSVGElement>) => void\n    onMouseMove?: (e: MouseEvent<SVGSVGElement>) => void\n}) {\n    const reactId = useId()\n    const id = `star-clip-${reactId.replace(/:/g, \"\")}`\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            width={px}\n            height={px}\n            onClick={onClick}\n            onMouseMove={onMouseMove}\n            aria-hidden=\"true\"\n            style={{ display: \"block\" }}\n        >\n            <defs>\n                <clipPath id={id}>\n                    <rect x=\"0\" y=\"0\" width={24 * fillPercent} height=\"24\" />\n                </clipPath>\n            </defs>\n            <path\n                d=\"M12 2.5l2.92 6.34 6.96.66-5.23 4.7 1.56 6.8L12 17.6l-6.21 3.4 1.56-6.8L2.12 9.5l6.96-.66z\"\n                fill=\"none\"\n                stroke={color}\n                strokeWidth=\"1.5\"\n                strokeLinejoin=\"round\"\n            />\n            <path\n                d=\"M12 2.5l2.92 6.34 6.96.66-5.23 4.7 1.56 6.8L12 17.6l-6.21 3.4 1.56-6.8L2.12 9.5l6.96-.66z\"\n                fill={color}\n                clipPath={`url(#${id})`}\n            />\n        </svg>\n    )\n}\n\n/**\n * Star rating input with half-star and read-only support.\n *\n * @component\n * @layer core\n */\nexport function Rating({\n    value,\n    defaultValue,\n    onChange,\n    count = 5,\n    allowHalf = false,\n    readOnly = false,\n    disabled = false,\n    size = \"md\",\n    color = \"var(--warning)\",\n    className,\n    surface,\n    radius,\n    animated,\n    iconWeight,\n    \"aria-label\": ariaLabel,\n}: RatingProps) {\n    const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n    const isControlled = value !== undefined\n    const [internal, setInternal] = useState<number>(defaultValue ?? 0)\n    const [hover, setHover] = useState<number | null>(null)\n    const current = isControlled ? (value as number) : internal\n    const display = hover ?? current\n    const px = SIZE_PX[size]\n    const interactive = !readOnly && !disabled\n\n    const commit = useCallback(\n        (next: number) => {\n            const clamped = Math.max(0, Math.min(count, next))\n            if (!isControlled) setInternal(clamped)\n            onChange?.(clamped)\n        },\n        [count, isControlled, onChange],\n    )\n\n    const computeScore = (i: number, e: MouseEvent<SVGSVGElement>): number => {\n        if (!allowHalf) return i + 1\n        const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect()\n        const halfway = rect.left + rect.width / 2\n        return e.clientX < halfway ? i + 0.5 : i + 1\n    }\n\n    return (\n        <div\n            data-slot=\"rating\"\n            data-size={size}\n            data-readonly={String(readOnly)}\n            data-disabled={String(disabled)}\n            data-surface={sf.surface}\n            data-radius={sf.radius}\n            data-animated={String(sf.animated)}\n            role={interactive ? \"slider\" : \"img\"}\n            aria-label={ariaLabel ?? `Rating: ${current} of ${count}`}\n            aria-valuemin={interactive ? 0 : undefined}\n            aria-valuemax={interactive ? count : undefined}\n            aria-valuenow={interactive ? current : undefined}\n            aria-valuetext={interactive ? `${current} of ${count} stars` : undefined}\n            tabIndex={interactive ? 0 : -1}\n            onKeyDown={\n                interactive\n                    ? (e) => {\n                          const inc = allowHalf ? 0.5 : 1\n                          if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n                              e.preventDefault()\n                              commit(current + inc)\n                          } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n                              e.preventDefault()\n                              commit(current - inc)\n                          } else if (e.key === \"Home\") {\n                              e.preventDefault()\n                              commit(0)\n                          } else if (e.key === \"End\") {\n                              e.preventDefault()\n                              commit(count)\n                          }\n                      }\n                    : undefined\n            }\n            onMouseLeave={interactive ? () => setHover(null) : undefined}\n            className={cn(\n                \"inline-flex items-center gap-0.5 outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\",\n                interactive && \"cursor-pointer\",\n                disabled && \"cursor-not-allowed opacity-50\",\n                className,\n            )}\n        >\n            {Array.from({ length: count }, (_, i) => {\n                const fillPercent = Math.max(0, Math.min(1, display - i))\n                return (\n                    <StarPath\n                        key={i}\n                        fillPercent={fillPercent}\n                        color={color}\n                        px={px}\n                        onClick={\n                            interactive\n                                ? (e) => commit(computeScore(i, e))\n                                : undefined\n                        }\n                        onMouseMove={\n                            interactive\n                                ? (e) => setHover(computeScore(i, e))\n                                : undefined\n                        }\n                    />\n                )\n            })}\n        </div>\n    )\n}\n",
      "target": "components/ui/rating.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/rating.json"
  }
}
