{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "content-carousel",
  "type": "registry:ui",
  "title": "Content Carousel",
  "description": "A Content Carousel component for displaying a carousel of content.",
  "files": [
    {
      "path": "src/registry/ui/content-carousel.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\n// Slides travel with the navigation direction: 1 = forward, -1 = back.\nconst slideVariants = {\n  enter: (direction: number) => ({ opacity: 0, x: direction * 24, filter: \"blur(2px)\" }),\n  center: { opacity: 1, x: 0, filter: \"blur(0px)\" },\n  exit: (direction: number) => ({ opacity: 0, x: direction * -24, filter: \"blur(2px)\" }),\n};\nconst fadeVariants = {\n  enter: { opacity: 0 },\n  center: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nexport type ContentCarouselItem = {\n  id: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n};\n\nexport type ContentCarouselProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\"\n> & {\n  items: ContentCarouselItem[];\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  /** Wrap from the last item to the first and back. */\n  loop?: boolean;\n};\n\nexport function ContentCarousel({\n  items,\n  index,\n  defaultIndex = 0,\n  onIndexChange,\n  loop = true,\n  className,\n  ...props\n}: ContentCarouselProps) {\n  const reduceMotion = useReducedMotion();\n  const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex);\n  const [direction, setDirection] = useState(1);\n  const count = items.length;\n  const current = Math.min(Math.max(index ?? uncontrolledIndex, 0), count - 1);\n\n  if (count === 0) return null;\n\n  const goTo = (next: number) => {\n    const target = loop\n      ? (next + count) % count\n      : Math.min(Math.max(next, 0), count - 1);\n    if (target === current) return;\n    // Use the requested step, not the wrapped target, so Next from the last item still moves forward.\n    setDirection(next > current ? 1 : -1);\n    if (index === undefined) setUncontrolledIndex(target);\n    onIndexChange?.(target);\n  };\n\n  const item = items[current];\n  const canGoPrev = loop || current > 0;\n  const canGoNext = loop || current < count - 1;\n\n  return (\n    <div\n      data-slot=\"content-carousel\"\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      className={cn(\"flex flex-col items-center gap-8 px-4\", className)}\n      {...props}\n    >\n      <div\n        aria-live=\"polite\"\n        aria-atomic=\"true\"\n        className=\"grid min-h-32 w-full max-w-2xl place-items-center text-center\"\n      >\n        {/* Outgoing and incoming items share one grid cell and overlap, so rapid clicks never queue. */}\n        <AnimatePresence initial={false} custom={direction}>\n          <motion.div\n            key={item.id}\n            custom={direction}\n            variants={reduceMotion ? fadeVariants : slideVariants}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n            transition={{ duration: reduceMotion ? 0.15 : 0.25, ease: EASE_OUT }}\n            className=\"space-y-3 [grid-area:1/1]\"\n          >\n            <h2 className=\"text-3xl font-semibold tracking-tight text-foreground sm:text-4xl\">\n              {item.title}\n            </h2>\n            {item.description && (\n              <div className=\"text-base text-muted-foreground sm:text-lg\">\n                {item.description}\n              </div>\n            )}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n\n      <div className=\"flex items-center gap-3\">\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          aria-label=\"Previous item\"\n          disabled={!canGoPrev}\n          onClick={() => goTo(current - 1)}\n        >\n          <ChevronLeft aria-hidden />\n        </Button>\n\n        <div className=\"flex items-center gap-1.5\">\n          {items.map((dot, i) => (\n            <button\n              key={dot.id}\n              type=\"button\"\n              aria-label={`Go to item ${i + 1}`}\n              aria-current={i === current ? \"true\" : undefined}\n              onClick={() => goTo(i)}\n              className={cn(\n                \"h-2 rounded-full bg-muted-foreground/30 outline-none transition-[width,background-color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-muted-foreground/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none\",\n                i === current ? \"w-6 bg-primary hover:bg-primary\" : \"w-2\"\n              )}\n            />\n          ))}\n        </div>\n\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          aria-label=\"Next item\"\n          disabled={!canGoNext}\n          onClick={() => goTo(current + 1)}\n        >\n          <ChevronRight aria-hidden />\n        </Button>\n      </div>\n\n      <p className=\"text-sm tabular-nums text-muted-foreground\">\n        {current + 1} / {count}\n      </p>\n    </div>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "button"
  ]
}
