{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "rating", "title": "Rating", "description": "A hand-drawn star rating with hover fill and hachure.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json" ], "files": [ { "path": "registry/new-york/ui/rating.tsx", "content": "\"use client\";\n\nimport { useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CrumbleContext,\n randomSeed,\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\n\nexport interface RatingProps extends CrumbleColorProps {\n className?: string;\n defaultValue?: number;\n disabled?: boolean;\n id?: string;\n label?: string;\n max?: number;\n onChange?: (value: number) => void;\n size?: number;\n theme?: CrumbleTheme;\n value?: number;\n}\n\nfunction starPath(size: number): string {\n const cx = size / 2;\n const cy = size / 2;\n const outer = size * 0.44;\n const inner = size * 0.18;\n const points = 5;\n let d = \"\";\n for (let i = 0; i < points * 2; i++) {\n const r = i % 2 === 0 ? outer : inner;\n const angle = (Math.PI / points) * i - Math.PI / 2;\n const x = cx + r * Math.cos(angle);\n const y = cy + r * Math.sin(angle);\n d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n }\n return d + \" Z\";\n}\n\nfunction Star({\n active,\n disabled,\n hovered,\n index,\n onClick,\n onHover,\n onLeave,\n ratingId,\n size,\n theme,\n}: {\n active: boolean;\n disabled: boolean;\n hovered: boolean;\n index: number;\n onClick: () => void;\n onHover: () => void;\n onLeave: () => void;\n ratingId: string;\n size: number;\n theme: CrumbleTheme;\n}) {\n const externalSvgRef = useRef(null);\n const path = starPath(size);\n const { drawPath, svgRef } = useRough({\n stableId: `${ratingId}-star-${index}`,\n svgRef: externalSvgRef,\n theme,\n variant: \"interactive\",\n });\n\n const draw = useCallback(\n (reseed = false) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n svg.replaceChildren();\n svg.setAttribute(\"width\", String(size));\n svg.setAttribute(\"height\", String(size));\n svg.setAttribute(\"viewBox\", `0 0 ${size} ${size}`);\n\n const filled = active || hovered;\n const star = drawPath(path, {\n fill: filled ? \"currentColor\" : \"none\",\n fillStyle: theme === \"ink\" ? \"solid\" : \"hachure\",\n seed: reseed ? randomSeed() : undefined,\n stroke: filled ? \"currentColor\" : \"var(--cr-stroke-muted)\",\n });\n if (star) svg.appendChild(star);\n },\n [active, drawPath, hovered, path, size, svgRef, theme],\n );\n\n useEffect(() => {\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw]);\n\n return (\n {\n onHover();\n draw(true);\n }}\n onMouseLeave={() => {\n onLeave();\n draw(false);\n }}\n className={cn(\n \"relative p-0.5 outline-none\",\n disabled ? \"cursor-not-allowed opacity-40\" : \"cursor-pointer\",\n )}\n style={{ width: size + 4, height: size + 4 }}\n >\n \n \n );\n}\n\nexport function Rating({\n className,\n defaultValue = 0,\n disabled = false,\n fill,\n id,\n label,\n max = 5,\n onChange,\n size = 24,\n stroke,\n strokeMuted,\n theme: themeProp,\n value: controlledValue,\n}: RatingProps) {\n const [internalValue, setInternalValue] = useState(defaultValue);\n const [hoverIndex, setHoverIndex] = useState(-1);\n\n const value = controlledValue ?? internalValue;\n const ratingId = id ?? `rating-${label ?? \"stars\"}`;\n const { theme: contextTheme } = useContext(CrumbleContext);\n const theme = themeProp ?? contextTheme;\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n\n const handleClick = (index: number) => {\n if (disabled) return;\n const next = index + 1;\n setInternalValue(next);\n onChange?.(next);\n };\n\n return (\n
\n {label ? (\n \n {label}\n \n ) : null}\n setHoverIndex(-1)}\n >\n {Array.from({ length: max }, (_, i) => (\n = 0 && i <= hoverIndex}\n disabled={disabled}\n ratingId={ratingId}\n size={size}\n theme={theme}\n onClick={() => handleClick(i)}\n onHover={() => setHoverIndex(i)}\n onLeave={() => setHoverIndex(-1)}\n />\n ))}\n
\n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/rating.tsx" } ], "type": "registry:ui" }