{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reviews-2",
  "title": "Reviews 2: ratings with insights",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Reviews with the whole picture: the average and the shape of the ratings (which filter the list), what people keep mentioning, and whether the mood is positive. The list follows the star size you choose.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/rating-distribution.json",
    "https://ui.ballmac.com/r/bar-list.json",
    "https://ui.ballmac.com/r/split-bar.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/reviews-2/reviews-2.tsx",
      "content": "// Ballmac UI: Reviews 2. https://ui.ballmac.com/blocks/reviews-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { Star } from \"lucide-react\"\n\nimport { BarList, type BarListItem } from \"@/components/ballmac/bar-list\"\nimport { RatingDistribution, type RatingCounts } from \"@/components/ballmac/rating-distribution\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { SplitBar, type SplitSegment } from \"@/components/ballmac/split-bar\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Reviews2Review = { id: string; author: string; rating: 1 | 2 | 3 | 4 | 5; date: string; title: string; body: string; verified?: boolean; helpful?: number }\n\ntype Reviews2Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    title?: string\n    description?: string\n    reviews?: Reviews2Review[]\n    /** Counts per size for the whole set. Defaults to counting the reviews given. */\n    counts?: RatingCounts\n    themes?: BarListItem[]\n    sentiment?: SplitSegment[]\n  }\n\nconst sampleReviews: Reviews2Review[] = [\n  { id: \"1\", author: \"Priya N.\", rating: 5, date: \"Oct 21\", title: \"Saved us a week of work\", body: \"We dropped the pricing block and the settings screens in and had a real product page the same afternoon. Keyboard navigation just worked.\", verified: true, helpful: 14 },\n  { id: \"2\", author: \"Tomás R.\", rating: 5, date: \"Oct 18\", title: \"The dark mode is actually good\", body: \"Most kits look fine in light and fall apart in dark. Every screen here held up, and the contrast is right.\", verified: true, helpful: 9 },\n  { id: \"3\", author: \"Hannah W.\", rating: 4, date: \"Oct 12\", title: \"Great, wish it had a Svelte version\", body: \"Everything we use is React so it fits. Documentation is clear, and the install command for each item is a nice touch.\", helpful: 5 },\n  { id: \"4\", author: \"Dmitri K.\", rating: 3, date: \"Oct 3\", title: \"Good, but the table was missing a feature\", body: \"The data table does most of what we need. Column pinning would have saved us a custom wrapper.\", verified: true, helpful: 3 },\n  { id: \"5\", author: \"Aisha B.\", rating: 2, date: \"Sep 28\", title: \"Needed more examples\", body: \"It took a while to find how to wire the form block to our API. The example for the error state helped once I found it.\", helpful: 2 },\n  { id: \"6\", author: \"Leo M.\", rating: 5, date: \"Sep 22\", title: \"Worth it for the agent screens alone\", body: \"The review desk and the run workbench are things I would have spent a month on. They are polished and accessible.\", verified: true, helpful: 11 },\n]\n\nconst sampleThemes: BarListItem[] = [\n  { id: \"a\", label: \"Saves time\", value: 412 },\n  { id: \"b\", label: \"Looks polished\", value: 356 },\n  { id: \"c\", label: \"Accessibility\", value: 241 },\n  { id: \"d\", label: \"Documentation\", value: 187 },\n  { id: \"e\", label: \"Missing a feature\", value: 64 },\n  { id: \"f\", label: \"Price\", value: 41 },\n]\n\nconst sampleSentiment: SplitSegment[] = [\n  { id: \"pos\", label: \"Positive\", value: 1610 },\n  { id: \"neu\", label: \"Mixed\", value: 390 },\n  { id: \"neg\", label: \"Negative\", value: 219 },\n]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]\">\n      <div className=\"grid gap-4\"><Skeleton className=\"h-40 rounded-xl\" />{[0, 1, 2].map((i) => <Skeleton key={i} className=\"h-28 rounded-xl\" />)}</div>\n      <div className=\"grid content-start gap-4\"><Skeleton className=\"h-56 rounded-xl\" /><Skeleton className=\"h-32 rounded-xl\" /></div>\n    </div>\n  )\n}\n\n/**\n * Reviews with the whole picture: the average and the shape of the ratings (which filter the list), what people keep mentioning, and whether the mood is\n * positive. The list below the summary follows the star size you choose.\n */\nfunction Reviews2({ title = \"What customers say\", description = \"Real reviews, and what they keep coming back to.\", reviews = sampleReviews, counts, themes = sampleThemes, sentiment = sampleSentiment, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Reviews2Props) {\n  const id = React.useId()\n  const [sel, setSel] = React.useState<1 | 2 | 3 | 4 | 5 | null>(null)\n  const view = state === \"ready\" && reviews.length === 0 ? \"empty\" : state\n  const tally = React.useMemo<RatingCounts>(() => counts ?? reviews.reduce((c, r) => ({ ...c, [r.rating]: c[r.rating] + 1 }), { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }), [counts, reviews])\n  const shown = sel ? reviews.filter((r) => r.rating === sel) : reviews\n\n  return (\n    <section data-slot=\"reviews-2\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-6\">\n        <h1 id={`${id}-title`} className=\"text-2xl font-semibold tracking-[-0.03em] text-balance\">{title}</h1>\n        <p className=\"mt-1 text-sm text-muted-foreground text-pretty\">{description}</p>\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading reviews\" loading={<Skel />} empty={{ title: \"No reviews yet\", description: \"When customers leave a review, it appears here.\", ...emptyState }} error={{ title: \"Reviews could not be loaded\", description: \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]\">\n          <div className=\"grid min-w-0 gap-4\">\n            <div className=\"rounded-xl border bg-card p-5 sm:p-6\">\n              <RatingDistribution counts={tally} selected={sel} onSelect={setSel} />\n            </div>\n            <p aria-live=\"polite\" className=\"text-sm text-muted-foreground\">\n              {sel ? `Showing ${shown.length} ${sel}-star ${shown.length === 1 ? \"review\" : \"reviews\"}.` : `Showing all ${shown.length} reviews.`}\n            </p>\n            <ul className=\"grid grid-cols-[minmax(0,1fr)] gap-3\">\n              {shown.map((r) => (\n                <li key={r.id} className=\"grid gap-2 rounded-xl border bg-card p-5\">\n                  <div className=\"flex flex-wrap items-center justify-between gap-2\">\n                    <p className=\"flex items-center gap-2 text-sm font-medium\">\n                      <span aria-hidden=\"true\" className=\"flex size-7 items-center justify-center rounded-full bg-muted text-xs font-semibold\">{r.author.charAt(0)}</span>\n                      {r.author}\n                      {r.verified && <span className=\"rounded-full border px-1.5 py-px text-[11px] font-medium text-muted-foreground\">Verified buyer</span>}\n                    </p>\n                    <p className=\"text-xs text-muted-foreground\">{r.date}</p>\n                  </div>\n                  <p className=\"flex items-center gap-2 text-sm font-semibold\">\n                    <span aria-hidden=\"true\" className=\"flex\">{[1, 2, 3, 4, 5].map((n) => <Star key={n} className={cn(\"size-3.5\", n <= r.rating ? \"fill-foreground text-foreground\" : \"text-border\")} />)}</span>\n                    <span className=\"sr-only\">{r.rating} out of 5 stars.</span>\n                    {r.title}\n                  </p>\n                  <p className=\"text-sm leading-relaxed text-muted-foreground text-pretty\">{r.body}</p>\n                  {r.helpful !== undefined && <p className=\"text-xs text-muted-foreground\">{r.helpful} people found this helpful</p>}\n                </li>\n              ))}\n              {shown.length === 0 && <li className=\"rounded-xl border border-dashed px-5 py-10 text-center text-sm text-muted-foreground\">No reviews with this rating.</li>}\n            </ul>\n          </div>\n          <div className=\"grid gap-4 lg:sticky lg:top-6\">\n            <div className=\"rounded-xl border bg-card p-5\"><BarList title=\"What people mention\" items={themes} showShare={false} /></div>\n            <div className=\"rounded-xl border bg-card p-5\"><SplitBar title=\"Overall mood\" segments={sentiment} /></div>\n          </div>\n        </div>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Reviews2, type Reviews2Props, type Reviews2Review }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/reviews-2/reviews-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "reviews",
      "ratings",
      "social proof",
      "insights",
      "sentiment",
      "filter",
      "ecommerce",
      "feedback"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Reviews with insights. <Reviews2 title description reviews={[{ id, author, rating, date, title, body, verified?, helpful? }]} counts themes={BarListItem[]} sentiment={SplitSegment[]} />. Counts default to the reviews given. Choosing a star size filters the list and the count is announced politely.",
      "whenToUse": [
        "A product or app page where reviews drive the decision",
        "A review dashboard for the team"
      ],
      "whenNotToUse": [
        "A scrolling quote wall for a landing page; use testimonials-1",
        "Collecting a review; use a form"
      ],
      "composesWith": [
        "analytics-1",
        "testimonials-1",
        "hero-1",
        "pricing-2"
      ],
      "a11y": [
        {
          "keys": "Filter",
          "action": "Each star row is a toggle button named for its count; the list's size is announced"
        },
        {
          "keys": "Reviews",
          "action": "Each is a list item; the star rating is spoken as 'n out of 5 stars'"
        },
        {
          "keys": "Charts",
          "action": "The bars and the mood split repeat numbers shown as text"
        }
      ],
      "customization": [
        "title and description",
        "reviews and counts",
        "themes and sentiment"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "reviews-2-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton summary, list and side panels under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No reviews yet",
        "example": "reviews-2-empty",
        "trigger": "state=\"empty\"",
        "note": "Says there are no reviews yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "reviews-2-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry)."
      }
    ],
    "examples": [
      "reviews-2-demo",
      "reviews-2-few",
      "reviews-2-loading",
      "reviews-2-empty",
      "reviews-2-error"
    ],
    "url": "https://ui.ballmac.com/components/reviews-2"
  },
  "categories": [
    "blocks",
    "reviews"
  ],
  "type": "registry:block"
}