{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "testimonials-2",
  "title": "Testimonials 2: featured quote with avatar picker",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "One large quote at a time with the person, role and a highlighted result. A row of avatars switches quotes with arrow keys; optional gentle autoplay that pauses on hover and focus.",
  "dependencies": [
    "lucide-react",
    "motion@^12",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/button.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/testimonials-2/testimonials-2.tsx",
      "content": "// Ballmac UI: Testimonials 2. https://ui.ballmac.com/blocks/testimonials-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowLeft, ArrowRight } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ballmac/avatar\"\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { cn } from \"@/lib/utils\"\n\ntype Testimonials2Item = {\n  /** The quote, without quotation marks. */\n  quote: string\n  /** Person's name. */\n  name: string\n  /** Their role. */\n  role: string\n  /** Their company. */\n  company: string\n  /** Profile photo. Initials are shown when omitted. */\n  image?: string\n  /** A result worth highlighting, e.g. \"−38% support tickets\". */\n  result?: string\n}\n\ntype Testimonials2Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\"> & {\n  /** Label above the quote. */\n  eyebrow?: string\n  /** The quotes. Three to five work best. */\n  items?: Testimonials2Item[]\n  /** Controlled index of the quote shown. */\n  value?: number\n  /** Index shown first when uncontrolled. */\n  defaultValue?: number\n  /** Called when the visitor picks another quote. */\n  onValueChange?: (index: number) => void\n  /** Milliseconds between automatic changes. 0 keeps it manual (the default). Pauses on hover and focus, and never runs with reduced motion. */\n  autoplay?: number\n}\n\nconst defaults: Testimonials2Item[] = [\n  {\n    quote: \"We replaced three tools and two spreadsheets with Acme in a single afternoon. Month-end close went from five days to one.\",\n    name: \"Priya Raman\",\n    role: \"VP Finance\",\n    company: \"Northwind\",\n    result: \"Close time down 80%\",\n  },\n  {\n    quote: \"The approval flow finally matches how our team actually works. Nobody asks where an invoice is anymore, because they can just look.\",\n    name: \"Marcus Webb\",\n    role: \"Head of Operations\",\n    company: \"Globex\",\n    result: \"−38% support tickets\",\n  },\n  {\n    quote: \"Setup took minutes and our auditors loved the trail. It is the first finance tool I have rolled out without a single training session.\",\n    name: \"Elena Fischer\",\n    role: \"Controller\",\n    company: \"Initech\",\n    result: \"Zero training sessions\",\n  },\n  {\n    quote: \"Our customers pay faster because the invoices look great and the payment link works on a phone. That alone paid for the year.\",\n    name: \"Tomás Herrera\",\n    role: \"Founder\",\n    company: \"Umbrella Studio\",\n    result: \"Paid 6 days faster\",\n  },\n]\n\nconst tones = [\"bg-chart-1/25\", \"bg-chart-3/25\", \"bg-chart-5/25\", \"bg-chart-2/25\", \"bg-chart-4/25\"]\nconst initials = (name: string) => name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n\nfunction Testimonials2({\n  eyebrow = \"Loved by finance teams\",\n  items = defaults,\n  value: valueProp,\n  defaultValue = 0,\n  onValueChange,\n  autoplay = 0,\n  className,\n  ...props\n}: Testimonials2Props) {\n  const [internal, setInternal] = React.useState(defaultValue)\n  const index = Math.min(Math.max(valueProp ?? internal, 0), Math.max(items.length - 1, 0))\n  const reduce = useReducedMotion()\n  const [paused, setPaused] = React.useState(false)\n  const count = items.length\n\n  const go = React.useCallback(\n    (next: number) => {\n      const wrapped = (next + count) % count\n      if (valueProp === undefined) setInternal(wrapped)\n      onValueChange?.(wrapped)\n    },\n    [count, onValueChange, valueProp]\n  )\n\n  React.useEffect(() => {\n    if (!autoplay || reduce || paused || count < 2) return\n    const id = window.setTimeout(() => go(index + 1), autoplay)\n    return () => window.clearTimeout(id)\n  }, [autoplay, reduce, paused, count, index, go])\n\n  const current = items[index]\n  if (!current) return null\n\n  return (\n    <section\n      data-slot=\"testimonials-2\"\n      className={cn(\"mx-auto max-w-5xl px-4 py-20 sm:px-6 md:py-28\", className)}\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocusCapture={() => setPaused(true)}\n      onBlurCapture={() => setPaused(false)}\n      {...props}\n    >\n      <TabsPrimitive.Root value={String(index)} onValueChange={(v) => go(Number(v))} className=\"flex flex-col items-center text-center\">\n        <Badge variant=\"outline\" className=\"rounded-full px-3 py-1\">{eyebrow}</Badge>\n\n        <div className=\"relative mt-6 w-full pt-16\">\n          <span aria-hidden=\"true\" className=\"text-chart-1/30 pointer-events-none absolute top-0 left-1/2 -translate-x-1/2 font-serif text-[7rem] leading-[0.8] select-none\">“</span>\n          {items.map((item, i) => (\n            <TabsPrimitive.Content key={i} value={String(i)} className=\"focus-visible:ring-ring/50 relative rounded-2xl outline-none focus-visible:ring-[3px]\">\n              <motion.figure\n                initial={reduce ? false : { opacity: 0, y: 12, filter: \"blur(6px)\" }}\n                animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}\n                className=\"mx-auto max-w-3xl\"\n              >\n                <blockquote className=\"text-2xl leading-[1.35] font-medium tracking-[-0.025em] text-balance sm:text-3xl lg:text-4xl\">\n                  {item.quote}\n                </blockquote>\n                <figcaption className=\"mt-8 flex flex-col items-center gap-3\">\n                  <span className=\"text-base\">\n                    <span className=\"font-semibold\">{item.name}</span>\n                    <span className=\"text-muted-foreground\">, {item.role} at {item.company}</span>\n                  </span>\n                  {item.result && <Badge status=\"success\">{item.result}</Badge>}\n                </figcaption>\n              </motion.figure>\n            </TabsPrimitive.Content>\n          ))}\n        </div>\n\n        <div className=\"mt-12 flex items-center gap-3\">\n          <Button variant=\"outline\" size=\"icon\" shape=\"pill\" aria-label=\"Previous testimonial\" onClick={() => go(index - 1)} className=\"hidden sm:inline-flex\">\n            <ArrowLeft  className=\"rtl:rotate-180\"/>\n          </Button>\n          <TabsPrimitive.List aria-label=\"Choose a testimonial\" className=\"flex items-center gap-1 rounded-full border p-1.5\">\n            {items.map((item, i) => (\n              <TabsPrimitive.Trigger\n                key={i}\n                value={String(i)}\n                aria-label={`${item.name}, ${item.company}`}\n                className=\"group/avatar focus-visible:ring-ring/50 rounded-full p-0.5 outline-none focus-visible:ring-[3px]\"\n              >\n                <Avatar className={cn(\"size-10 transition-all duration-300 group-data-[state=inactive]/avatar:scale-90 group-data-[state=inactive]/avatar:opacity-75 group-data-[state=active]/avatar:ring-2 group-data-[state=active]/avatar:ring-foreground group-data-[state=active]/avatar:ring-offset-2 group-data-[state=active]/avatar:ring-offset-background motion-reduce:transition-none\")}>\n                  {item.image && <AvatarImage src={item.image} alt=\"\" />}\n                  <AvatarFallback className={cn(\"text-foreground text-xs font-semibold\", tones[i % tones.length])}>{initials(item.name)}</AvatarFallback>\n                </Avatar>\n              </TabsPrimitive.Trigger>\n            ))}\n          </TabsPrimitive.List>\n          <Button variant=\"outline\" size=\"icon\" shape=\"pill\" aria-label=\"Next testimonial\" onClick={() => go(index + 1)} className=\"hidden sm:inline-flex\">\n            <ArrowRight  className=\"rtl:rotate-180\"/>\n          </Button>\n        </div>\n      </TabsPrimitive.Root>\n    </section>\n  )\n}\n\nexport { Testimonials2, type Testimonials2Props, type Testimonials2Item }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/testimonials-2/testimonials-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "testimonials",
      "quote",
      "carousel",
      "social proof",
      "avatars"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A focused testimonial section. Pass items=[{ quote, name, role, company, image?, result? }]; control the index with value/onValueChange or let it manage itself.",
      "whenToUse": [
        "Three to five strong quotes",
        "Above a pricing or CTA section"
      ],
      "whenNotToUse": [
        "Many short reviews (use testimonials-1)"
      ],
      "composesWith": [
        "stats-1",
        "pricing-2",
        "cta-1",
        "logo-cloud-1"
      ],
      "a11y": [
        {
          "keys": "Arrow Left / Right",
          "action": "Moves to the previous or next quote"
        },
        {
          "keys": "Home / End",
          "action": "Jumps to the first or last quote"
        },
        {
          "keys": "Tab",
          "action": "Moves from the avatars into the quote"
        }
      ],
      "customization": [
        "items: { quote, name, role, company, image?, result? }[]",
        "autoplay: milliseconds between quotes (0 = manual)",
        "value / defaultValue / onValueChange"
      ]
    },
    "examples": [
      "testimonials-2-demo",
      "testimonials-2-autoplay"
    ],
    "url": "https://ui.ballmac.com/components/testimonials-2"
  },
  "categories": [
    "blocks",
    "testimonials"
  ],
  "type": "registry:block"
}