{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "rating",
  "title": "Rating",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/rating/Rating.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface RatingProps extends HTMLAttributes<HTMLDivElement> {\n    /** Currently selected value. Bindable. */\n    value?: number\n    /** Maximum rating value */\n    max?: number\n    /** If true, prevents user interaction */\n    readonly?: boolean\n    /** If true, disables the rating */\n    disabled?: boolean\n    /** Density of the component */\n    density?: 'compact' | 'default' | 'comfortable'\n    /** Color of the selected stars */\n    color?: string\n    /** If true, clicking the same value clears the rating */\n    clearable?: boolean\n    /** If true, stars grow on hover */\n    hover?: boolean\n    /** ARIA label for each rating item */\n    itemAriaLabel?: string\n    /** Size of the stars */\n    size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'\n    /** Show the numeric value next to the stars */\n    showValue?: boolean\n    /** Card variant styling */\n    variant?: 'outlined' | 'filled' | 'soft'\n    /** If true, creates a half star at 0.5 */\n    halfIncrements?: boolean\n    /** If true, displays tooltips on hover */\n    tooltips?: string[]\n    /** Called with the new value after every commit (click or keyboard). */\n    onValueChange?: (value: number) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    value = $bindable(0),\n    max = 5,\n    readonly = false,\n    disabled = false,\n    density = 'default',\n    color = 'var(--warning)',\n    clearable = false,\n    hover = false,\n    itemAriaLabel = 'rating',\n    size = 'medium',\n    showValue = false,\n    variant = 'outlined',\n    halfIncrements = false,\n    tooltips,\n    onValueChange,\n    ref = $bindable(null),\n    ...restProps\n  }: RatingProps = $props()\n\n  const instanceId = $props.id()\n\n  const densityClasses = {\n    compact: 'rating-density-compact',\n    default: 'rating-density-default',\n    comfortable: 'rating-density-comfortable',\n  }\n\n  const variantClasses = {\n    outlined: 'rating-variant-outlined',\n    filled: 'rating-variant-filled',\n    soft: 'rating-variant-soft',\n  }\n\n  const sizeClasses = {\n    'x-small': 'rating-size-xs',\n    small: 'rating-size-sm',\n    medium: 'rating-size-md',\n    large: 'rating-size-lg',\n    'x-large': 'rating-size-xl',\n  }\n\n  function commit(next: number) {\n    value = next\n    onValueChange?.(next)\n  }\n\n  function resolveClickValue(event: MouseEvent, star: number): number {\n    if (!halfIncrements) return star\n    const target = event.currentTarget as HTMLElement\n    const rect = target.getBoundingClientRect()\n    const isLeft = event.clientX - rect.left < rect.width / 2\n    const next = isLeft ? star - 0.5 : star\n    return next < 0.5 ? 0.5 : next\n  }\n\n  function handleClick(event: MouseEvent, star: number) {\n    if (disabled || readonly) return\n    const next = resolveClickValue(event, star)\n    if (clearable && next === value) {\n      commit(0)\n    } else {\n      commit(next)\n    }\n  }\n\n  function handleKeydown(event: KeyboardEvent, star: number) {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      if (disabled || readonly) return\n      if (clearable && star === value) {\n        commit(0)\n      } else {\n        commit(star)\n      }\n      return\n    }\n    const step = halfIncrements ? 0.5 : 1\n    if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {\n      event.preventDefault()\n      commit(Math.min(max, (value ?? 0) + step))\n    } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {\n      event.preventDefault()\n      commit(Math.max(0, (value ?? 0) - step))\n    } else if (event.key === 'Home') {\n      event.preventDefault()\n      commit(halfIncrements ? 0.5 : 1)\n    } else if (event.key === 'End') {\n      event.preventDefault()\n      commit(max)\n    }\n  }\n\n  function starClass(index: number): string {\n    if (value >= index + 1) return 'rating-star rating-star-full'\n    if (value >= index + 0.5 && halfIncrements) return 'rating-star rating-star-half'\n    return 'rating-star rating-star-empty'\n  }\n\n  /** Stagger cascade only for filled stars (0-based index). */\n  function starDelay(index: number): string {\n    if (value < index + 1) return '0ms'\n    return `${index * 45}ms`\n  }\n\n  const stars = $derived(Array.from({ length: max }, (_, i) => i + 1))\n</script>\n\n<!-- aria-valuenow/min/max mirror the Vue twin (svelte's role table omits radiogroup). -->\n<!-- svelte-ignore a11y_role_supports_aria_props -->\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"rating\"\n  class={cn(\n    'rating',\n    densityClasses[density],\n    variantClasses[variant],\n    sizeClasses[size],\n    readonly && 'rating-readonly',\n    disabled && 'rating-disabled',\n    hover && 'rating-hover',\n    clearable && 'rating-clearable',\n    showValue && 'rating-show-value',\n    className,\n  )}\n  style=\"--rating-color: {color}\"\n  role=\"radiogroup\"\n  aria-valuenow={value}\n  aria-valuemin={0}\n  aria-valuemax={max}\n  aria-label={`Rating: ${value} of ${max}`}\n  {...restProps}\n>\n  {#each stars as n (n)}\n    <button\n      type=\"button\"\n      role=\"radio\"\n      class={cn('focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none', starClass(n - 1))}\n      style=\"--star-delay: {starDelay(n - 1)}\"\n      disabled={disabled || readonly}\n      aria-label={tooltips?.[n - 1] ?? `${itemAriaLabel} ${n} of ${max}`}\n      aria-checked={Math.ceil(value || 0) === n}\n      title={tooltips?.[n - 1]}\n      tabindex={readonly || disabled ? -1 : Math.ceil(value || 1) === n ? 0 : -1}\n      onclick={(e) => handleClick(e, n)}\n      onkeydown={(e) => handleKeydown(e, n)}\n    >\n      {#if value >= n}\n        <!-- Full Star Icon -->\n        <svg class=\"star-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" style=\"color: {color}\">\n          <path d=\"M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\" />\n        </svg>\n      {:else if value >= n - 0.5 && halfIncrements}\n        <!-- Half Star Icon -->\n        <svg class=\"star-icon star-half\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" style=\"color: {color}\">\n          <defs>\n            <linearGradient id=\"{instanceId}-half-{n}\">\n              <stop offset=\"50%\" stop-color=\"currentColor\" />\n              <stop offset=\"50%\" stop-color=\"transparent\" />\n            </linearGradient>\n          </defs>\n          <path\n            d=\"M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n            fill=\"url(#{instanceId}-half-{n})\"\n            stroke=\"currentColor\"\n            stroke-width=\"1\"\n          />\n        </svg>\n      {:else}\n        <!-- Empty Star Icon -->\n        <svg\n          class=\"star-icon\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"1.5\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\" />\n        </svg>\n      {/if}\n    </button>\n  {/each}\n  {#if showValue}\n    <span class=\"rating-value\">{value}</span>\n  {/if}\n</div>\n\n<style>\n  .rating {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.125rem;\n  }\n\n  .rating-readonly {\n    cursor: default;\n  }\n\n  .rating-disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  .rating-star {\n    transition: transform 0.15s ease-in-out;\n  }\n\n  .rating:not(.rating-disabled):not(.rating-readonly) .rating-star:hover {\n    transform: scale(1.12);\n  }\n\n  .rating-hover .rating-star:hover {\n    transform: scale(1.18);\n  }\n\n  .rating-clearable .rating-star {\n    cursor: pointer;\n  }\n\n  .rating-variant-filled {\n    background: var(--muted);\n    padding: 0.25rem;\n    border-radius: 0.5rem;\n  }\n\n  .rating-variant-soft {\n    background: var(--accent);\n    padding: 0.25rem;\n    border-radius: 0.5rem;\n  }\n\n  .rating-star {\n    background: none;\n    border: none;\n    padding: 0.125rem;\n    line-height: 1;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .rating-star:focus-visible {\n    outline: 2px solid var(--rating-color);\n    outline-offset: 2px;\n    border-radius: 2px;\n  }\n\n  /* Density */\n  .rating-density-compact .rating-star {\n    padding: 0;\n  }\n\n  .rating-density-default .rating-star {\n    padding: 0.0625rem;\n  }\n\n  .rating-density-comfortable .rating-star {\n    padding: 0.125rem;\n  }\n\n  /* Sizes */\n  .rating-size-xs .star-icon {\n    width: 0.875rem;\n    height: 0.875rem;\n  }\n\n  .rating-size-sm .star-icon {\n    width: 1.125rem;\n    height: 1.125rem;\n  }\n\n  .rating-size-md .star-icon {\n    width: 1.375rem;\n    height: 1.375rem;\n  }\n\n  .rating-size-lg .star-icon {\n    width: 1.625rem;\n    height: 1.625rem;\n  }\n\n  .rating-size-xl .star-icon {\n    width: 2rem;\n    height: 2rem;\n  }\n\n  .rating-star-empty {\n    color: var(--muted-foreground);\n  }\n\n  .rating-star-full {\n    color: var(--rating-color);\n  }\n\n  .rating-star-full .star-icon {\n    animation: rating-star-pop 280ms cubic-bezier(0.22, 1.4, 0.36, 1) both;\n    animation-delay: var(--star-delay, 0ms);\n  }\n\n  @keyframes rating-star-pop {\n    0% {\n      opacity: 0.4;\n      transform: scale(0.6);\n    }\n    60% {\n      opacity: 1;\n      transform: scale(1.18);\n    }\n    100% {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .rating-star-full .star-icon {\n      animation: none !important;\n    }\n    .rating-star {\n      transition: none !important;\n    }\n  }\n\n  .rating-star-half .star-icon {\n    position: relative;\n  }\n\n  .rating-value {\n    margin-left: 0.5rem;\n    font-weight: 600;\n    color: var(--foreground);\n  }\n\n  .rating-show-value {\n    display: flex;\n    align-items: center;\n    gap: 0.25rem;\n  }\n</style>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/rating/Rating.svelte"
    },
    {
      "path": "packages/registry-svelte/components/rating/index.ts",
      "content": "export { default as Rating, type RatingProps } from './Rating.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/rating/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Star (or custom icon) rating control — pick a value from 1 to N. Read-only mode for displaying review averages, with half-step support.",
  "categories": [
    "form"
  ]
}