"use client"; import { Star } from "lucide-react"; import { useId, useState } from "react"; import { cn } from "@/lib/utils"; export type RatingSize = "sm" | "md" | "lg"; export interface RatingProps { /** Controlled value, from 0 to `max`. */ value?: number; /** Starting value when uncontrolled. Default 0. */ defaultValue?: number; /** Called with the new value each time a star is picked. */ onValueChange?: (value: number) => void; /** How many stars. Default 5. */ max?: number; /** Shows the value without letting anyone change it. Default false. */ readOnly?: boolean; /** Default "md". */ size?: RatingSize; /** Names the group for screen readers. Default "Rating". */ label?: string; className?: string; } const SIZES: Record = { sm: "h-4 w-4", md: "h-6 w-6", lg: "h-8 w-8" }; /** * A row of stars. Hovering previews the score, picking one fills the row up to it in a quick * wave, and the arrow keys move it one star at a time. Built on native radio inputs. */ export function Rating({ value, defaultValue = 0, onValueChange, max = 5, readOnly = false, size = "md", label = "Rating", className, }: RatingProps) { const name = useId(); const [inner, setInner] = useState(defaultValue); const current = value ?? inner; const [hover, setHover] = useState(null); // Bumped on every pick, so the pop replays even when the same star is picked twice. const [wave, setWave] = useState(0); const shown = hover ?? current; const pick = (next: number) => { if (readOnly) return; setInner(next); setWave((n) => n + 1); onValueChange?.(next); }; return (
setHover(null)} disabled={readOnly} className={cn("m-0 inline-flex min-w-0 items-center gap-0.5 border-0 p-0", className)} > {label} {Array.from({ length: max }, (_, index) => index + 1).map((score) => { const on = score <= shown; return ( ); })}
); }