{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "rating",
  "title": "Rating",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/rating/rating.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  Output,\n  ViewEncapsulation,\n  booleanAttribute,\n  numberAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { uniqueId } from '@/ui/popper/popper'\n\nexport type RatingDensity = 'compact' | 'default' | 'comfortable'\nexport type RatingSize = 'x-small' | 'small' | 'medium' | 'large' | 'x-large'\nexport type RatingVariant = 'outlined' | 'filled' | 'soft'\n\n// Star path shared by all three icon states.\nconst STAR_PATH = '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\nconst variantClasses: Record<RatingVariant, string> = {\n  outlined: '',\n  filled: 'bg-muted p-1 rounded-lg',\n  soft: 'bg-accent p-1 rounded-lg',\n}\n\nconst sizeIcon: Record<RatingSize, string> = {\n  'x-small': '0.875rem',\n  small: '1.125rem',\n  medium: '1.375rem',\n  large: '1.625rem',\n  'x-large': '2rem',\n}\n\nconst densityPad: Record<RatingDensity, string> = {\n  compact: '0',\n  default: '0.0625rem',\n  comfortable: '0.125rem',\n}\n\n// Copied verbatim from the React Rating (injected there as a global <style>).\nconst STYLE_CONTENT = `\n@keyframes rating-star-pop {\n  0% { opacity: 0.4; transform: scale(0.6); }\n  60% { opacity: 1; transform: scale(1.18); }\n  100% { opacity: 1; transform: scale(1); }\n}\n[data-slot='rating'] .rating-star-full-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[data-slot='rating'] button:not(:disabled):hover {\n  transform: scale(1.12);\n  transition: transform 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  [data-slot='rating'] .rating-star-full-icon {\n    animation: none !important;\n  }\n  [data-slot='rating'] button:not(:disabled):hover {\n    transition: none !important;\n    transform: none !important;\n  }\n}\n`\n\n/**\n * Angular port of the React Rating: a radiogroup of star buttons (role=\"radio\", roving\n * tabindex on the selected star), with half-star resolution from the click position when\n * `halfIncrements`, Arrow / Home / End / Enter / Space keys, `clearable` reset, per-star\n * tooltips and the pop-in animation. `value` / `defaultValue` / `valueChange` (`[(value)]`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-rating, [ui-rating]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  styles: [STYLE_CONTENT],\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'rating',\n    role: 'radiogroup',\n    '[attr.aria-valuenow]': 'current',\n    'aria-valuemin': '0',\n    '[attr.aria-valuemax]': 'max',\n    '[attr.aria-label]': '\"Rating: \" + current + \" of \" + max',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @for (n of stars; track n) {\n      <button\n        type=\"button\"\n        role=\"radio\"\n        [disabled]=\"disabled || readonly\"\n        [attr.aria-label]=\"tooltips?.[n - 1] ?? itemAriaLabel + ' ' + n + ' of ' + max\"\n        [attr.aria-checked]=\"ceil(current || 0) === n\"\n        [attr.title]=\"tooltips?.[n - 1] ?? null\"\n        [attr.tabindex]=\"readonly || disabled ? -1 : focusStar === n ? 0 : -1\"\n        [style.padding]=\"padding\"\n        [style.outline-color]=\"color\"\n        [style.--star-delay]=\"current >= n ? (n - 1) * 45 + 'ms' : '0ms'\"\n        [class]=\"buttonClass\"\n        (click)=\"handleClick($event, n)\"\n        (keydown)=\"handleKeydown($event, n)\"\n      >\n        @if (current >= n) {\n          <svg\n            class=\"rating-star-full-icon\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            aria-hidden=\"true\"\n            [style.width]=\"iconSize\"\n            [style.height]=\"iconSize\"\n            [style.color]=\"color\"\n          >\n            <path [attr.d]=\"starPath\" />\n          </svg>\n        } @else if (current >= n - 0.5 && halfIncrements) {\n          <svg\n            viewBox=\"0 0 24 24\"\n            aria-hidden=\"true\"\n            [style.width]=\"iconSize\"\n            [style.height]=\"iconSize\"\n            [style.color]=\"color\"\n          >\n            <defs>\n              <linearGradient [attr.id]=\"halfId(n)\">\n                <stop offset=\"50%\" stop-color=\"currentColor\" />\n                <stop offset=\"50%\" stop-color=\"transparent\" />\n              </linearGradient>\n            </defs>\n            <path [attr.d]=\"starPath\" [attr.fill]=\"'url(#' + halfId(n) + ')'\" stroke=\"currentColor\" stroke-width=\"1\" />\n          </svg>\n        } @else {\n          <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"1.5\"\n            aria-hidden=\"true\"\n            [style.width]=\"iconSize\"\n            [style.height]=\"iconSize\"\n            class=\"text-muted-foreground\"\n          >\n            <path [attr.d]=\"starPath\" />\n          </svg>\n        }\n      </button>\n    }\n    @if (showValue) {\n      <span class=\"text-foreground ml-2 font-semibold\">{{ current }}</span>\n    }\n  `,\n})\nexport class UiRatingComponent {\n  private readonly _value = signal<number | undefined>(undefined)\n  private readonly _internal = signal<number | null>(null)\n  private readonly idBase = uniqueId('rating')\n\n  /** Currently selected value (React `value`); reading it returns the resolved value. */\n  @Input()\n  set value(v: number | null | undefined) {\n    this._value.set(v ?? undefined)\n  }\n  get value(): number {\n    return this._value() ?? this._internal() ?? this.defaultValue\n  }\n  /** Default value when uncontrolled. */\n  @Input({ transform: numberAttribute }) defaultValue = 0\n  @Output() valueChange = new EventEmitter<number>()\n  @Input({ transform: numberAttribute }) max = 5\n  @Input({ transform: booleanAttribute }) readonly = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input() density: RatingDensity = 'default'\n  /** Color of the selected stars. */\n  @Input() color = 'var(--warning)'\n  /** Clicking the selected value clears the rating. */\n  @Input({ transform: booleanAttribute }) clearable = false\n  /** Stars grow on hover. */\n  @Input({ transform: booleanAttribute }) hover = false\n  @Input() itemAriaLabel = 'rating'\n  @Input() size: RatingSize = 'medium'\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) showValue = false\n  @Input() variant: RatingVariant = 'outlined'\n  @Input({ transform: booleanAttribute }) halfIncrements = false\n  /** Optional per-star tooltip labels (native title + aria-label). */\n  @Input() tooltips?: string[]\n\n  readonly starPath = STAR_PATH\n  readonly ceil = Math.ceil\n\n  get current(): number {\n    return this.value\n  }\n\n  get stars(): number[] {\n    return Array.from({ length: Math.max(0, this.max) }, (_, i) => i + 1)\n  }\n\n  get focusStar(): number {\n    return Math.ceil(this.current || 1)\n  }\n\n  get iconSize(): string {\n    return sizeIcon[this.size]\n  }\n\n  get padding(): string {\n    return densityPad[this.density]\n  }\n\n  get hostClass(): string {\n    return cn(\n      'inline-flex items-center gap-0.5',\n      variantClasses[this.variant],\n      this.disabled && 'cursor-not-allowed opacity-50',\n      this.readonly && 'cursor-default',\n      this.showValue && 'flex items-center gap-1',\n      this.className,\n    )\n  }\n\n  get buttonClass(): string {\n    return cn(\n      'inline-flex items-center justify-center border-none bg-none leading-none transition-transform duration-150',\n      'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',\n      this.clearable && 'cursor-pointer',\n      this.hover && 'hover:scale-[1.15]',\n    )\n  }\n\n  halfId(n: number): string {\n    return `${this.idBase}-half-${n}`\n  }\n\n  private emit(next: number): void {\n    if (this._value() === undefined) this._internal.set(next)\n    this.valueChange.emit(next)\n  }\n\n  private resolveClickValue(e: MouseEvent, star: number): number {\n    if (!this.halfIncrements) return star\n    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()\n    const isLeft = e.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  handleClick(e: MouseEvent, star: number): void {\n    if (this.disabled || this.readonly) return\n    const next = this.resolveClickValue(e, star)\n    if (this.clearable && next === this.current) this.emit(0)\n    else this.emit(next)\n  }\n\n  handleKeydown(e: KeyboardEvent, star: number): void {\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      if (this.disabled || this.readonly) return\n      if (this.clearable && star === this.current) this.emit(0)\n      else this.emit(star)\n      return\n    }\n    const step = this.halfIncrements ? 0.5 : 1\n    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n      e.preventDefault()\n      this.emit(Math.min(this.max, this.current + step))\n    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n      e.preventDefault()\n      this.emit(Math.max(0, this.current - step))\n    } else if (e.key === 'Home') {\n      e.preventDefault()\n      this.emit(this.halfIncrements ? 0.5 : 1)\n    } else if (e.key === 'End') {\n      e.preventDefault()\n      this.emit(this.max)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/rating/rating.component.ts"
    },
    {
      "path": "packages/registry-angular/components/rating/index.ts",
      "content": "export { UiRatingComponent, type RatingDensity, type RatingSize, type RatingVariant } from './rating.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/rating/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Star rating control (radiogroup of star buttons) with half-star support, keyboard control, clearable selection, tooltips, read-only display, sizes, densities and variants.",
  "categories": [
    "form"
  ]
}