{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-testimonials",
  "title": "Animated Testimonials",
  "description": "A testimonial carousel with a shuffling image stack, word-by-word quote reveal, and autoplay.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/animated-testimonials/animated-testimonials.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type Testimonial = {\n  /** The testimonial quote. */\n  quote: string;\n  /** Person's name. */\n  name: string;\n  /** Person's role / company. */\n  role: string;\n  /** Avatar / portrait image URL. */\n  src: string;\n};\n\nexport type AnimatedTestimonialsProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Testimonials to cycle through. */\n  testimonials: Testimonial[];\n  /** Advance automatically. */\n  autoplay?: boolean;\n  /** Autoplay interval in milliseconds. */\n  interval?: number;\n};\n\nconst ARROW_BASE =\n  \"flex size-9 items-center justify-center rounded-full bg-muted text-foreground [transition:background_200ms_ease] hover:bg-accent\";\n\nconst AnimatedTestimonials = React.forwardRef<\n  HTMLDivElement,\n  AnimatedTestimonialsProps\n>(\n  (\n    { testimonials, autoplay = true, interval = 5000, className, ...props },\n    ref,\n  ) => {\n    const [active, setActive] = React.useState(0);\n    const count = testimonials.length;\n\n    const next = React.useCallback(\n      () => setActive((prev) => (prev + 1) % count),\n      [count],\n    );\n    const prev = () => setActive((p) => (p - 1 + count) % count);\n\n    // Stable per-item rotation so inactive cards fan out consistently.\n    const rotations = React.useMemo(\n      () => testimonials.map(() => Math.floor(Math.random() * 16) - 8),\n      [testimonials],\n    );\n\n    // Keyed on `active` so the countdown restarts whenever the slide changes —\n    // including manual prev / next — instead of firing on a fixed cadence.\n    // biome-ignore lint/correctness/useExhaustiveDependencies: `active` is the intentional reset trigger\n    React.useEffect(() => {\n      if (!autoplay || count <= 1) return;\n      const id = setTimeout(next, interval);\n      return () => clearTimeout(id);\n    }, [autoplay, interval, next, count, active]);\n\n    if (count === 0) return null;\n    const current = testimonials[active];\n\n    return (\n      <div\n        ref={ref}\n        data-slot=\"animated-testimonials\"\n        className={`grid w-full max-w-3xl gap-8 md:grid-cols-2 ${className ?? \"\"}`}\n        {...props}\n      >\n        <div className=\"relative h-72 [perspective:1000px]\">\n          <AnimatePresence>\n            {testimonials.map((t, i) => {\n              const isActive = i === active;\n              return (\n                <motion.img\n                  // biome-ignore lint/suspicious/noArrayIndexKey: stack position is the identity here\n                  key={t.src + i}\n                  src={t.src}\n                  alt={t.name}\n                  draggable={false}\n                  initial={{ opacity: 0, scale: 0.9, rotate: rotations[i] }}\n                  animate={{\n                    opacity: isActive ? 1 : 0.5,\n                    scale: isActive ? 1 : 0.92,\n                    rotate: isActive ? 0 : rotations[i],\n                    zIndex: isActive ? count : count - Math.abs(active - i),\n                    y: isActive ? 0 : 8,\n                  }}\n                  exit={{ opacity: 0, scale: 0.9 }}\n                  transition={{\n                    type: \"spring\",\n                    stiffness: 320,\n                    damping: 32,\n                    mass: 0.9,\n                  }}\n                  className=\"absolute inset-0 size-full rounded-2xl object-cover shadow-lg\"\n                />\n              );\n            })}\n          </AnimatePresence>\n        </div>\n\n        <div className=\"flex flex-col justify-between\">\n          <AnimatePresence mode=\"wait\">\n            <motion.div\n              key={active}\n              initial={{ opacity: 0, y: 12 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -12 }}\n              transition={{ duration: 0.2 }}\n            >\n              <p className=\"text-lg leading-relaxed text-foreground\">\n                {current.quote.split(\" \").map((word, i) => (\n                  <motion.span\n                    // biome-ignore lint/suspicious/noArrayIndexKey: words are positional within a quote\n                    key={`${active}-${i}`}\n                    initial={{ opacity: 0, filter: \"blur(6px)\", y: 6 }}\n                    animate={{ opacity: 1, filter: \"blur(0px)\", y: 0 }}\n                    transition={{ duration: 0.2, delay: 0.02 * i }}\n                    className=\"inline-block\"\n                  >\n                    {word}&nbsp;\n                  </motion.span>\n                ))}\n              </p>\n              <div className=\"mt-4\">\n                <div className=\"font-semibold text-foreground\">\n                  {current.name}\n                </div>\n                <div className=\"text-sm text-muted-foreground\">\n                  {current.role}\n                </div>\n              </div>\n            </motion.div>\n          </AnimatePresence>\n\n          <div className=\"mt-8 flex gap-3\">\n            <button\n              type=\"button\"\n              aria-label=\"Previous testimonial\"\n              onClick={prev}\n              className={ARROW_BASE}\n            >\n              ‹\n            </button>\n            <button\n              type=\"button\"\n              aria-label=\"Next testimonial\"\n              onClick={next}\n              className={ARROW_BASE}\n            >\n              ›\n            </button>\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\nAnimatedTestimonials.displayName = \"AnimatedTestimonials\";\n\nexport { AnimatedTestimonials };\n",
      "type": "registry:ui",
      "target": "components/godui/animated-testimonials.tsx"
    }
  ],
  "type": "registry:ui"
}
