{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-testimonials",
  "title": "Animated Testimonials",
  "description": "Crossfades portraits behind a rotating quote.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/animated-testimonials.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface Testimonial {\n  quote: string;\n  name: string;\n  role: string;\n  /** Optional image URL; falls back to the person's initials */\n  src?: string;\n}\n\ninterface AnimatedTestimonialsProps {\n  testimonials: Testimonial[];\n  /** Seconds between slides; 0 disables autoplay */\n  interval?: number;\n  className?: string;\n}\n\nconst initials = (name: string) =>\n  name\n    .split(\" \")\n    .map((part) => part[0])\n    .slice(0, 2)\n    .join(\"\");\n\n/**\n * Testimonial carousel that crossfades portraits behind the quote.\n * Autoplay pauses on hover and focus, has a pause button, and is off under\n * `prefers-reduced-motion`; the controls still work.\n */\nexport function AnimatedTestimonials({\n  testimonials,\n  interval = 6,\n  className,\n}: AnimatedTestimonialsProps) {\n  const [index, setIndex] = useState(0);\n  const [paused, setPaused] = useState(false);\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const reducedMotion = useReducedMotion();\n  const count = testimonials.length;\n  const autoplay = Boolean(interval) && count > 1;\n\n  useEffect(() => {\n    if (!autoplay || reducedMotion || paused || hovered || focused) return;\n    const id = window.setInterval(\n      () => setIndex((i) => (i + 1) % count),\n      interval * 1000\n    );\n    return () => window.clearInterval(id);\n  }, [autoplay, interval, count, reducedMotion, paused, hovered, focused]);\n\n  if (!count) return null;\n  const active = testimonials[index];\n  const duration = reducedMotion ? 0 : 0.35;\n\n  return (\n    <div\n      data-slot=\"animated-testimonials\"\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={() => setFocused(true)}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n          setFocused(false);\n        }\n      }}\n      className={cn(\"flex flex-col gap-6 sm:flex-row sm:items-center\", className)}\n    >\n      <div className=\"relative size-24 shrink-0 sm:size-32\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.div\n            key={index}\n            initial={{ opacity: 0, scale: 0.92, rotate: -4 }}\n            animate={{ opacity: 1, scale: 1, rotate: 0 }}\n            exit={{ opacity: 0, scale: 0.92, rotate: 4 }}\n            transition={{ duration, ease: \"easeOut\" }}\n            className=\"absolute inset-0 overflow-hidden rounded-2xl border bg-muted\"\n          >\n            {active.src ? (\n              // eslint-disable-next-line @next/next/no-img-element\n              <img loading=\"lazy\" decoding=\"async\"\n                src={active.src}\n                alt={active.name}\n                className=\"size-full object-cover\"\n              />\n            ) : (\n              <span className=\"flex size-full items-center justify-center bg-gradient-to-br from-brand-from to-brand-to text-2xl font-semibold text-brand-foreground\">\n                {initials(active.name)}\n              </span>\n            )}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n\n      <div className=\"min-w-0 flex-1\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.blockquote\n            key={index}\n            initial={{ opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -8 }}\n            transition={{ duration, ease: \"easeOut\" }}\n          >\n            <p className=\"text-lg leading-relaxed text-balance\">\n              &ldquo;{active.quote}&rdquo;\n            </p>\n            <footer className=\"mt-3 text-sm text-muted-foreground\">\n              <span className=\"font-medium text-foreground\">{active.name}</span>\n              {\" — \"}\n              {active.role}\n            </footer>\n          </motion.blockquote>\n        </AnimatePresence>\n\n        {count > 1 && (\n          <div className=\"mt-4 flex items-center\">\n            {autoplay && (\n              // Hidden by CSS under reduced motion, where autoplay is off.\n              <button\n                type=\"button\"\n                onClick={() => setPaused((value) => !value)}\n                aria-label={paused ? \"Play testimonials\" : \"Pause testimonials\"}\n                className=\"mr-2 grid size-6 place-items-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hidden\"\n              >\n                <svg\n                  width=\"12\"\n                  height=\"12\"\n                  viewBox=\"0 0 12 12\"\n                  fill=\"currentColor\"\n                  aria-hidden\n                >\n                  {paused ? (\n                    <path d=\"M3 1.5v9l7.5-4.5z\" />\n                  ) : (\n                    <path d=\"M2.5 1.5h2.5v9H2.5zM7 1.5h2.5v9H7z\" />\n                  )}\n                </svg>\n              </button>\n            )}\n            {/* Each 24px button is the hit area; the pill inside is visual. */}\n            {testimonials.map((testimonial, i) => (\n              <button\n                key={testimonial.name}\n                type=\"button\"\n                onClick={() => setIndex(i)}\n                aria-label={`Show testimonial from ${testimonial.name}`}\n                aria-current={i === index}\n                className=\"group/dot grid h-6 min-w-6 place-items-center rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <span\n                  className={cn(\n                    \"h-1.5 rounded-full transition-all motion-reduce:transition-none\",\n                    i === index\n                      ? \"w-6 bg-brand\"\n                      : \"w-1.5 bg-muted-foreground/30 group-hover/dot:bg-muted-foreground/60\"\n                  )}\n                />\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/animated-testimonials.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-brand": "var(--brand)",
      "color-brand-from": "var(--brand-from)",
      "color-brand-via": "var(--brand-via)",
      "color-brand-to": "var(--brand-to)"
    },
    "light": {
      "brand": "oklch(0.546 0.245 263)",
      "brand-from": "oklch(0.546 0.245 263)",
      "brand-via": "oklch(0.623 0.214 260)",
      "brand-to": "oklch(0.685 0.169 237)"
    },
    "dark": {
      "brand": "oklch(0.623 0.214 260)",
      "brand-from": "oklch(0.623 0.214 260)",
      "brand-via": "oklch(0.707 0.165 255)",
      "brand-to": "oklch(0.746 0.16 233)"
    }
  },
  "type": "registry:ui"
}