{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "testimonial",
  "type": "registry:ui",
  "title": "Testimonial",
  "description": "Quote + attribution surfaces. Single Testimonial cells render an blockquote with an author avatar; TestimonialGrid lays them out in a 1-3 column grid. Server components.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "marketing",
    "pattern"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/avatar.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/container.json",
    "https://ds.interlace.tools/r/grid.json",
    "https://ds.interlace.tools/r/skeleton.json",
    "https://ds.interlace.tools/r/stack.json",
    "https://ds.interlace.tools/r/typography.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/testimonial.tsx",
      "target": "components/ui/patterns/testimonial.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/testimonial v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/testimonial\n// What changed since: https://ds.interlace.tools/c/testimonial#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — Testimonial + TestimonialGrid\n *\n * Quote + attribution surfaces. Single Testimonial cells render an\n * blockquote with an author avatar; TestimonialGrid lays them out in\n * a 1-3 column grid. Server components.\n *\n * ## Anatomy\n *\n *   Testimonial:\n *     <figure data-slot=\"testimonial\" data-min-viewport=\"320\">\n *       <blockquote>{quote}</blockquote>\n *       <figcaption>\n *         <Avatar /> <cite>{author}</cite> · <span>{role}</span>\n *       </figcaption>\n *     </figure>\n *\n *   TestimonialGrid:\n *     <section data-slot=\"testimonial-grid\"><Container>\n *       <Grid cols={3}>{items.map(<Testimonial/>)}</Grid>\n *     </Container></section>\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | Testimonial extends `<figure>`; Grid extends `<section>`    |\n * | R6   | data-slot on root                | `data-slot=\"testimonial\"` / `data-slot=\"testimonial-grid\"`  |\n * | R10  | Composition seam                 | TestimonialGrid takes `items: Testimonial[]`                |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`                                         |\n * | R19  | Tokens only                      | bg-card, border-border, semantic foreground                 |\n * | R25  | Server component                 | No hooks                                                    |\n * | R26  | A11y                             | semantic <blockquote> + <cite>                              |\n */\n\nimport { cn } from '@/lib/utils';\nimport { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';\nimport { Container } from '@/components/ui/container';\nimport { Grid } from '@/components/ui/grid';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Stack } from '@/components/ui/stack';\nimport { Typography } from '@/components/ui/typography';\n\nexport const MIN_VIEWPORT = 320 as const;\n\ninterface TestimonialItem {\n  quote: React.ReactNode;\n  authorName: string;\n  authorRole?: React.ReactNode;\n  authorAvatar?: string;\n}\n\ninterface TestimonialProps\n  extends Omit<React.ComponentProps<'figure'>, 'children'>,\n    TestimonialItem {\n  /** When true, render a placeholder card. */\n  loading?: boolean;\n}\n\nfunction initialOf(name: string): string {\n  const parts = name.trim().split(/\\s+/);\n  return (parts[0]?.[0] ?? '?').toUpperCase() + (parts[1]?.[0] ?? '').toUpperCase();\n}\n\nfunction Testimonial({\n  quote,\n  authorName,\n  authorRole,\n  authorAvatar,\n  loading,\n  className,\n  ...props\n}: TestimonialProps) {\n  if (loading) {\n    return (\n      <Skeleton\n        variant=\"card\"\n        data-slot=\"testimonial\"\n        data-min-viewport={String(MIN_VIEWPORT)}\n        className={className}\n      />\n    );\n  }\n  return (\n    <figure\n      data-slot=\"testimonial\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn(\n        'border-border bg-card text-card-foreground flex flex-col gap-md rounded-lg border p-md',\n        className,\n      )}\n      {...props}\n    >\n      <blockquote className=\"text-body leading-body text-foreground\">\n        “{quote}”\n      </blockquote>\n      <figcaption className=\"flex items-center gap-sm\">\n        <Avatar className=\"size-9 shrink-0\">\n          {authorAvatar ? (\n            <AvatarImage src={authorAvatar} alt=\"\" />\n          ) : null}\n          <AvatarFallback>{initialOf(authorName)}</AvatarFallback>\n        </Avatar>\n        <div className=\"flex min-w-0 flex-col\">\n          <cite className=\"font-body text-ui font-semibold not-italic text-foreground\">\n            {authorName}\n          </cite>\n          {authorRole ? (\n            <span className=\"text-muted-foreground text-sm\">{authorRole}</span>\n          ) : null}\n        </div>\n      </figcaption>\n    </figure>\n  );\n}\nTestimonial.displayName = 'Testimonial';\n\ninterface TestimonialGridProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n  title?: React.ReactNode;\n  lead?: React.ReactNode;\n  items?: TestimonialItem[];\n  cols?: 1 | 2 | 3;\n  loading?: boolean;\n  loadingCount?: number;\n}\n\n/**\n * Mobile-first track counts per desktop `cols`. Written out statically because\n * Tailwind cannot scan a runtime-built `sm:grid-cols-${n}`.\n */\nconst TESTIMONIAL_GRID_COLS: Record<1 | 2 | 3, string> = {\n  1: 'grid-cols-1',\n  2: 'grid-cols-1 sm:grid-cols-2',\n  3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',\n};\n\nfunction TestimonialGrid({\n  title,\n  lead,\n  items = [],\n  cols = 3,\n  loading,\n  loadingCount,\n  className,\n  ...props\n}: TestimonialGridProps) {\n  const skeletonCount = loadingCount ?? cols;\n  return (\n    <section\n      data-slot=\"testimonial-grid\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn('py-xl', className)}\n      {...props}\n    >\n      <Container size=\"content\">\n        <Stack gap=\"lg\">\n          {(title || lead) && (\n            <Stack gap=\"sm\" align=\"center\" className=\"text-center\">\n              {title ? (\n                <Typography variant=\"h2\" as=\"h2\" className=\"text-balance\">\n                  {title}\n                </Typography>\n              ) : null}\n              {lead ? (\n                <Typography variant=\"long\" tone=\"muted\" className=\"max-w-prose\">\n                  {lead}\n                </Typography>\n              ) : null}\n            </Stack>\n          )}\n          {/* Responsive collapse: `cols` is a DESKTOP count. Unqualified it\n              keeps 3 tracks at 375px, squeezing each quote card until the\n              author name and role collapse to zero width and vanish. */}\n          <Grid cols={cols} gap=\"md\" className={TESTIMONIAL_GRID_COLS[cols]}>\n            {loading\n              ? Array.from({ length: skeletonCount }).map((_, i) => (\n                  <Skeleton key={i} variant=\"card\" label={null} />\n                ))\n              : items.map((item, i) => <Testimonial key={i} {...item} />)}\n          </Grid>\n        </Stack>\n      </Container>\n    </section>\n  );\n}\nTestimonialGrid.displayName = 'TestimonialGrid';\n\nexport { Testimonial, TestimonialGrid };\nexport type { TestimonialProps, TestimonialGridProps, TestimonialItem };\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": false,
    "minViewport": 320,
    "loading": true,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/testimonial\n\nInstalled to `components/ui/patterns/testimonial.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/testimonial';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/testimonial\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
