{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rating-distribution",
  "title": "Rating Distribution",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The shape of a set of ratings: the average with partly filled stars, the count, and a bar per star size that can filter the reviews beside it.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/rating-distribution.tsx",
      "content": "// Ballmac UI: Rating Distribution. https://ui.ballmac.com/components/rating-distribution\n\"use client\"\n\nimport * as React from \"react\"\nimport { Star } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype RatingCounts = { 1: number; 2: number; 3: number; 4: number; 5: number }\n\ntype RatingDistributionProps = Omit<React.ComponentProps<\"div\">, \"onSelect\"> & {\n  /** How many ratings of each size. */\n  counts: RatingCounts\n  /** The selected size, to filter reviews with. Controlled. */\n  selected?: 1 | 2 | 3 | 4 | 5 | null\n  /** Makes each row a button. Called with the size, or null when it is cleared. */\n  onSelect?: (rating: 1 | 2 | 3 | 4 | 5 | null) => void\n}\n\nconst nf = new Intl.NumberFormat(\"en-US\")\nconst STARS = [5, 4, 3, 2, 1] as const\n\n/**\n * The shape of a set of ratings: the average, the count, and a bar for each star size. Rows can be buttons that filter the reviews beside them. The average is\n * written as a number and the stars are decoration; a partly filled star shows the decimals.\n */\nfunction RatingDistribution({ counts, selected, onSelect, className, ...props }: RatingDistributionProps) {\n  const msg = useMessages()\n  const total = STARS.reduce((s, k) => s + counts[k], 0)\n  const avg = total ? STARS.reduce((s, k) => s + k * counts[k], 0) / total : 0\n  const max = Math.max(1, ...STARS.map((k) => counts[k]))\n\n  return (\n    <div data-slot=\"rating-distribution\" className={cn(\"grid min-w-0 gap-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:gap-8\", className)} {...props}>\n      <div className=\"grid content-center gap-1.5\">\n        <p className=\"text-5xl font-semibold tracking-[-0.04em] tabular-nums\">{avg ? avg.toFixed(1) : \"–\"}</p>\n        <div aria-hidden=\"true\" className=\"flex gap-0.5\">\n          {[1, 2, 3, 4, 5].map((n) => {\n            const fill = Math.min(1, Math.max(0, avg - (n - 1)))\n            return (\n              <span key={n} className=\"relative size-5\">\n                <Star className=\"absolute inset-0 size-5 text-border\" />\n                <span className=\"absolute inset-y-0 start-0 overflow-hidden\" style={{ width: `${fill * 100}%` }}>\n                  <Star className=\"size-5 fill-foreground text-foreground\" />\n                </span>\n              </span>\n            )\n          })}\n        </div>\n        <p className=\"text-sm text-muted-foreground\">{msg(\"rating-distribution.total\", \"{count} ratings, average {avg} out of 5\", { count: nf.format(total), avg: avg ? avg.toFixed(1) : \"0\" })}</p>\n      </div>\n      <ul className=\"grid grid-cols-[minmax(0,1fr)] content-center gap-1\">\n        {STARS.map((k) => {\n          const on = selected === k\n          const n = counts[k]\n          const content = (\n            <>\n              <span className=\"flex w-8 shrink-0 items-center gap-1 text-sm font-medium tabular-nums\">\n                {k}\n                <Star aria-hidden=\"true\" className=\"size-3 fill-current\" />\n              </span>\n              <span aria-hidden=\"true\" className=\"h-2 min-w-0 flex-1 overflow-hidden rounded-full bg-muted\">\n                <span className={cn(\"block h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none\", on || selected == null ? \"bg-foreground\" : \"bg-muted-foreground/40\")} style={{ width: `${(n / max) * 100}%` }} />\n              </span>\n              <span className=\"w-20 shrink-0 text-end text-sm tabular-nums text-muted-foreground\">\n                {nf.format(n)}\n                <span className=\"ms-1.5 text-xs\">{msg(\"rating-distribution.share\", \"{percent}%\", { percent: total ? Math.round((n / total) * 100) : 0 })}</span>\n              </span>\n            </>\n          )\n          return (\n            <li key={k}>\n              {onSelect ? (\n                <button type=\"button\" aria-pressed={on} aria-label={msg(\"rating-distribution.row\", \"{stars} stars: {count} ratings. Show only these\", { stars: k, count: nf.format(n) })} onClick={() => onSelect(on ? null : k)} className=\"flex min-h-9 w-full items-center gap-3 rounded-md px-2 text-start outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-pressed:bg-accent\">\n                  {content}\n                </button>\n              ) : (\n                <div className=\"flex min-h-9 items-center gap-3 px-2\">{content}</div>\n              )}\n            </li>\n          )\n        })}\n      </ul>\n    </div>\n  )\n}\n\nexport { RatingDistribution, type RatingCounts, type RatingDistributionProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/rating-distribution.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "reviews",
      "rating",
      "stars",
      "distribution",
      "histogram",
      "feedback",
      "ecommerce"
    ],
    "version": "1.0.0",
    "updated": "2026-10-25",
    "tests": true,
    "ai": {
      "summary": "counts is { 1, 2, 3, 4, 5 } (how many of each). The average and total are computed and written as text. onSelect(rating | null) with selected makes each row a toggle button for filtering a review list.",
      "whenToUse": [
        "Above a list of product, app or course reviews",
        "A feedback summary"
      ],
      "whenNotToUse": [
        "Collecting a rating; use rating",
        "Satisfaction over time; use chart-line"
      ],
      "composesWith": [
        "rating",
        "bar-list",
        "split-bar",
        "stat-card"
      ],
      "a11y": [
        {
          "keys": "Summary",
          "action": "The average and total are text; the stars are decoration"
        },
        {
          "keys": "Rows",
          "action": "Buttons named '4 stars: 2 ratings. Show only these' with pressed state"
        },
        {
          "keys": "Counts",
          "action": "Shown as numbers and percents beside each bar"
        }
      ],
      "customization": [
        "counts",
        "onSelect and selected"
      ]
    },
    "statesNone": "A presentational summary: the counts are passed in, so it has nothing of its own to load.",
    "examples": [
      "rating-distribution-demo",
      "rating-distribution-static"
    ],
    "url": "https://ui.ballmac.com/components/rating-distribution"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}