'use client';
import * as React from 'react';
import { cn } from '@/registry/default/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { Star } from 'lucide-react';
const ratingVariants = cva('flex items-center', {
variants: {
size: {
sm: 'gap-2',
md: 'gap-2.5',
lg: 'gap-3',
},
},
defaultVariants: {
size: 'md',
},
});
const starVariants = cva('', {
variants: {
size: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
},
},
defaultVariants: {
size: 'md',
},
});
const valueVariants = cva('text-muted-foreground w-5', {
variants: {
size: {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base',
},
},
defaultVariants: {
size: 'md',
},
});
function Rating({
rating,
maxRating = 5,
size,
className,
starClassName,
showValue = false,
editable = false,
onRatingChange,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof ratingVariants> & {
/**
* Current rating value (supports decimal values for partial stars)
*/
rating: number;
/**
* Maximum rating value (number of stars to show)
*/
maxRating?: number;
/**
* Whether to show the numeric rating value
*/
showValue?: boolean;
/**
* Class name for the value span
*/
starClassName?: string;
/**
* Whether the rating is editable (clickable)
*/
editable?: boolean;
/**
* Callback function called when rating changes
*/
onRatingChange?: (rating: number) => void;
}) {
const [hoveredRating, setHoveredRating] = React.useState<number | null>(null);
const displayRating = editable && hoveredRating !== null ? hoveredRating : rating;
const handleStarClick = (starRating: number) => {
if (editable && onRatingChange) {
onRatingChange(starRating);
}
};
const handleStarMouseEnter = (starRating: number) => {
if (editable) {
setHoveredRating(starRating);
}
};
const handleStarMouseLeave = () => {
if (editable) {
setHoveredRating(null);
}
};
const renderStars = () => {
const stars = [];
for (let i = 1; i <= maxRating; i++) {
const filled = displayRating >= i;
const partiallyFilled = displayRating > i - 1 && displayRating < i;
const fillPercentage = partiallyFilled ? (displayRating - (i - 1)) * 100 : 0;
stars.push(
<div
key={i}
className={cn('relative', editable && 'cursor-pointer')}
onClick={() => handleStarClick(i)}
onMouseEnter={() => handleStarMouseEnter(i)}
onMouseLeave={handleStarMouseLeave}
>
{/* Background star (empty) */}
<Star data-slot="rating-star-empty" className={cn(starVariants({ size }), 'text-muted-foreground/30')} />
{/* Filled star */}
<div
className="absolute inset-0 overflow-hidden"
style={{
width: filled ? '100%' : `${fillPercentage}%`,
}}
>
<Star
data-slot="rating-star-filled"
className={cn(starVariants({ size }), 'text-yellow-400 fill-yellow-400')}
/>
</div>
</div>,
);
}
return stars;
};
return (
<div data-slot="rating" className={cn(ratingVariants({ size }), className)} {...props}>
<div className="flex items-center">{renderStars()}</div>
{showValue && (
<span data-slot="rating-value" className={cn(valueVariants({ size }), starClassName)}>
{displayRating.toFixed(1)}
</span>
)}
</div>
);
}
export { Rating };