{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "testimonials",
  "title": "Testimonials",
  "description": "Customer testimonials on a dark brand band — a motion crossfade carousel or a static grid.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@nx-ui/grid-backdrop",
    "@nx-ui/motion",
    "@nx-ui/section",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/marketing/testimonials.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeft, ChevronRight } from \"@untitledui/icons\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { GridBackdrop } from \"@/components/blocks/marketing/grid-backdrop\"\nimport { Eyebrow } from \"@/components/blocks/marketing/section\"\nimport { EASE } from \"@/components/blocks/marketing/motion\"\n\n/** A single testimonial. */\nexport interface Testimonial {\n  id: string\n  quote: string\n  name: string\n  title?: string\n  avatarUrl?: string\n}\n\nconst ROTATE_MS = 8000\n\nfunction Stars() {\n  return (\n    <div aria-hidden=\"true\" className=\"flex gap-1\">\n      {Array.from({ length: 5 }).map((_, i) => (\n        <svg key={i} viewBox=\"0 0 24 24\" className=\"size-5 fill-accent-300\">\n          <path d=\"M12 2.5l2.9 5.88 6.49.94-4.7 4.58 1.11 6.46L12 17.85 6.2 20.36l1.11-6.46-4.7-4.58 6.49-.94z\" />\n        </svg>\n      ))}\n    </div>\n  )\n}\n\nfunction Avatar({ item }: { item: Testimonial }) {\n  if (!item.avatarUrl) {\n    return (\n      <span\n        aria-hidden=\"true\"\n        className=\"flex size-12 items-center justify-center rounded-full bg-white/10 text-sm font-semibold text-white ring-2 ring-white/15\"\n      >\n        {item.name.charAt(0)}\n      </span>\n    )\n  }\n  return (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      src={item.avatarUrl}\n      alt=\"\"\n      width={48}\n      height={48}\n      className=\"size-12 rounded-full object-cover ring-2 ring-white/15\"\n    />\n  )\n}\n\ninterface TestimonialsProps {\n  items: Testimonial[]\n  eyebrow?: string\n  title?: string\n  /** Milliseconds between auto-advances (carousel only). */\n  autoRotateMs?: number\n  /** `carousel` auto-rotates one quote; `grid` shows a static card wall. */\n  variant?: \"carousel\" | \"grid\"\n  className?: string\n}\n\n/**\n * Testimonials — social proof on a dark brand band. `carousel` (default) rotates\n * a single quote with a motion crossfade, auto-advancing (paused under\n * `prefers-reduced-motion`) with manual prev/next + dot navigation. `grid`\n * renders every quote as a static card wall — a motion-free fallback.\n */\nfunction Testimonials({\n  items,\n  eyebrow = \"In their words\",\n  title = \"Teams already trust us.\",\n  autoRotateMs = ROTATE_MS,\n  variant = \"carousel\",\n  className,\n}: TestimonialsProps) {\n  const reduce = useReducedMotion()\n  const [index, setIndex] = React.useState(0)\n  const count = items.length\n\n  const go = React.useCallback(\n    (next: number) => setIndex((prev) => (next + count) % count),\n    [count]\n  )\n\n  React.useEffect(() => {\n    if (variant !== \"carousel\" || reduce || count <= 1) return\n    const id = window.setInterval(\n      () => setIndex((prev) => (prev + 1) % count),\n      autoRotateMs\n    )\n    return () => window.clearInterval(id)\n  }, [variant, reduce, count, autoRotateMs])\n\n  if (count === 0) return null\n\n  return (\n    <section\n      data-slot=\"testimonials\"\n      className={cn(\"relative overflow-hidden bg-brand-600\", className)}\n    >\n      <GridBackdrop\n        tone=\"brand\"\n        mask=\"radial-gradient(ellipse at center, rgba(0,0,0,0.6) 20%, transparent 75%)\"\n      />\n      <div className=\"relative mx-auto flex w-full max-w-container flex-col items-center gap-10 px-4 py-20 text-center md:px-8 md:py-28\">\n        {eyebrow ? <Eyebrow onBrand>{eyebrow}</Eyebrow> : null}\n        <h2 className=\"max-w-3xl text-display-sm font-semibold tracking-tight text-balance text-white md:text-display-md\">\n          {title}\n        </h2>\n\n        {variant === \"grid\" ? (\n          <div className=\"grid w-full gap-6 md:grid-cols-2 lg:grid-cols-3\">\n            {items.map((item) => (\n              <figure\n                key={item.id}\n                className=\"flex flex-col gap-5 rounded-2xl bg-white/5 p-6 text-left ring-1 ring-white/10\"\n              >\n                <Stars />\n                <blockquote className=\"text-md text-white/90\">\n                  “{item.quote}”\n                </blockquote>\n                <figcaption className=\"mt-auto flex items-center gap-3\">\n                  <Avatar item={item} />\n                  <span className=\"flex flex-col\">\n                    <span className=\"text-sm font-semibold text-white\">\n                      {item.name}\n                    </span>\n                    {item.title ? (\n                      <cite className=\"text-sm text-white/60 not-italic\">\n                        {item.title}\n                      </cite>\n                    ) : null}\n                  </span>\n                </figcaption>\n              </figure>\n            ))}\n          </div>\n        ) : (\n          <>\n            <div className=\"flex min-h-64 w-full max-w-3xl flex-col items-center justify-center gap-8\">\n              <AnimatePresence mode=\"wait\" initial={false}>\n                <motion.figure\n                  key={items[index].id}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14 }}\n                  transition={{ duration: reduce ? 0.15 : 0.45, ease: EASE }}\n                  className=\"flex flex-col items-center gap-8\"\n                >\n                  <Stars />\n                  <blockquote className=\"text-xl font-medium text-balance text-white md:text-display-xs md:leading-tight\">\n                    “{items[index].quote}”\n                  </blockquote>\n                  <figcaption className=\"flex items-center gap-3\">\n                    <Avatar item={items[index]} />\n                    <span className=\"flex flex-col text-left\">\n                      <span className=\"text-sm font-semibold text-white\">\n                        {items[index].name}\n                      </span>\n                      {items[index].title ? (\n                        <cite className=\"text-sm text-white/60 not-italic\">\n                          {items[index].title}\n                        </cite>\n                      ) : null}\n                    </span>\n                  </figcaption>\n                </motion.figure>\n              </AnimatePresence>\n            </div>\n\n            {count > 1 ? (\n              <div className=\"flex items-center gap-6\">\n                <button\n                  type=\"button\"\n                  onClick={() => go(index - 1)}\n                  aria-label=\"Previous testimonial\"\n                  className=\"flex size-9 cursor-pointer items-center justify-center rounded-full text-white/70 ring-1 ring-white/20 transition duration-150 ease-linear outline-none hover:bg-white/10 hover:text-white focus-visible:ring-4 focus-visible:ring-white/25\"\n                >\n                  <ChevronLeft className=\"size-4\" />\n                </button>\n                <div className=\"flex items-center gap-2\">\n                  {items.map((item, i) => (\n                    <button\n                      key={item.id}\n                      type=\"button\"\n                      onClick={() => setIndex(i)}\n                      aria-label={`Go to testimonial ${i + 1}`}\n                      aria-current={i === index}\n                      className={cn(\n                        \"h-2 cursor-pointer rounded-full transition-all duration-200 ease-linear\",\n                        i === index\n                          ? \"w-6 bg-accent-300\"\n                          : \"w-2 bg-white/25 hover:bg-white/40\"\n                      )}\n                    />\n                  ))}\n                </div>\n                <button\n                  type=\"button\"\n                  onClick={() => go(index + 1)}\n                  aria-label=\"Next testimonial\"\n                  className=\"flex size-9 cursor-pointer items-center justify-center rounded-full text-white/70 ring-1 ring-white/20 transition duration-150 ease-linear outline-none hover:bg-white/10 hover:text-white focus-visible:ring-4 focus-visible:ring-white/25\"\n                >\n                  <ChevronRight className=\"size-4\" />\n                </button>\n              </div>\n            ) : null}\n          </>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport { Testimonials }\n",
      "type": "registry:component",
      "target": "components/blocks/marketing/testimonials.tsx"
    }
  ],
  "type": "registry:block"
}