{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "A lightweight carousel component with CSS transitions.",
  "dependencies": ["lucide-react"],
  "files": [
    {
      "path": "registry/components/ui/carousel.tsx",
      "content": "\"use client\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronUp,\n} from \"lucide-react\";\nimport {\n  Children,\n  ReactNode,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type CarouselContextType = {\n  index: number;\n  setIndex: (newIndex: number | ((prev: number) => number)) => void;\n  itemsCount: number;\n  setItemsCount: (newItemsCount: number) => void;\n  disableDrag: boolean;\n  loop: boolean;\n  orientation: \"horizontal\" | \"vertical\";\n};\n\nconst CarouselContext = createContext<CarouselContextType | undefined>(\n  undefined,\n);\nfunction useCarousel() {\n  const context = useContext(CarouselContext);\n  if (!context) {\n    throw new Error(\"useCarousel must be used within an CarouselProvider\");\n  }\n  return context;\n}\n\nexport type CarouselProviderProps = {\n  children: ReactNode;\n  initialIndex?: number;\n  onIndexChange?: (newIndex: number) => void;\n  disableDrag?: boolean;\n  loop?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n};\n\nfunction CarouselProvider({\n  children,\n  initialIndex = 0,\n  onIndexChange,\n  disableDrag = false,\n  loop = false,\n  orientation = \"horizontal\",\n}: CarouselProviderProps) {\n  const [index, setIndex] = useState<number>(initialIndex);\n  const [itemsCount, setItemsCount] = useState<number>(0);\n\n  const handleSetIndex = useCallback(\n    (newIndex: number | ((prev: number) => number)) => {\n      if (typeof newIndex === \"function\") {\n        setIndex((prev) => {\n          const next = newIndex(prev);\n          onIndexChange?.(next);\n          return next;\n        });\n      } else {\n        setIndex(newIndex);\n        onIndexChange?.(newIndex);\n      }\n    },\n    [onIndexChange],\n  );\n\n  useEffect(() => {\n    setIndex(initialIndex);\n  }, [initialIndex]);\n\n  const contextValue = useMemo(\n    () => ({\n      index,\n      setIndex: handleSetIndex,\n      itemsCount,\n      setItemsCount,\n      disableDrag,\n      loop,\n      orientation,\n    }),\n    [index, handleSetIndex, itemsCount, disableDrag, loop, orientation],\n  );\n\n  return (\n    <CarouselContext.Provider value={contextValue}>\n      {children}\n    </CarouselContext.Provider>\n  );\n}\n\nexport type CarouselProps = {\n  children: ReactNode;\n  className?: string;\n  initialIndex?: number;\n  index?: number;\n  onIndexChange?: (newIndex: number) => void;\n  disableDrag?: boolean;\n  loop?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n};\n\nfunction Carousel({\n  children,\n  className,\n  initialIndex = 0,\n  index: externalIndex,\n  onIndexChange,\n  disableDrag = false,\n  loop = false,\n  orientation = \"horizontal\",\n}: CarouselProps) {\n  const [internalIndex, setInternalIndex] = useState<number>(initialIndex);\n  const isControlled = externalIndex !== undefined;\n  const currentIndex = isControlled ? externalIndex : internalIndex;\n\n  const handleIndexChange = (newIndex: number) => {\n    if (!isControlled) {\n      setInternalIndex(newIndex);\n    }\n    onIndexChange?.(newIndex);\n  };\n\n  return (\n    <CarouselProvider\n      initialIndex={currentIndex}\n      onIndexChange={handleIndexChange}\n      disableDrag={disableDrag}\n      loop={loop}\n      orientation={orientation}\n    >\n      <div className={cn(\"group/hover relative h-full\", className)}>\n        <div className=\"h-full overflow-hidden\">{children}</div>\n      </div>\n    </CarouselProvider>\n  );\n}\n\nexport type CarouselNavigationProps = {\n  className?: string;\n  classNameButton?: string;\n  alwaysShow?: boolean;\n};\n\nfunction CarouselNavigation({\n  className,\n  classNameButton,\n  alwaysShow,\n}: CarouselNavigationProps) {\n  const { index, setIndex, itemsCount, loop, orientation } = useCarousel();\n  const isVertical = orientation === \"vertical\";\n\n  const handlePrevClick = () => {\n    if (index > 0) {\n      setIndex(index - 1);\n    } else if (loop) {\n      setIndex(itemsCount - 1);\n    }\n  };\n\n  const handleNextClick = () => {\n    if (index < itemsCount - 1) {\n      setIndex(index + 1);\n    } else if (loop) {\n      setIndex(0);\n    }\n  };\n\n  const PrevIcon = isVertical ? ChevronUp : ChevronLeft;\n  const NextIcon = isVertical ? ChevronDown : ChevronRight;\n\n  return (\n    <div\n      className={cn(\n        \"pointer-events-none absolute flex justify-between\",\n        isVertical\n          ? \"left-1/2 top-[-12.5%] h-[125%] -translate-x-1/2 flex-col py-2\"\n          : \"left-[-12.5%] top-1/2 w-[125%] -translate-y-1/2 px-2\",\n        className,\n      )}\n    >\n      <button\n        type=\"button\"\n        aria-label=\"Previous slide\"\n        className={cn(\n          \"pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950\",\n          alwaysShow\n            ? \"opacity-100\"\n            : \"opacity-0 group-hover/hover:opacity-100\",\n          alwaysShow\n            ? \"disabled:opacity-40\"\n            : \"group-hover/hover:disabled:opacity-40\",\n          classNameButton,\n        )}\n        disabled={!loop && index === 0}\n        onClick={handlePrevClick}\n      >\n        <PrevIcon className=\"stroke-zinc-600 dark:stroke-zinc-50\" size={16} />\n      </button>\n      <button\n        type=\"button\"\n        className={cn(\n          \"pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950\",\n          alwaysShow\n            ? \"opacity-100\"\n            : \"opacity-0 group-hover/hover:opacity-100\",\n          alwaysShow\n            ? \"disabled:opacity-40\"\n            : \"group-hover/hover:disabled:opacity-40\",\n          classNameButton,\n        )}\n        aria-label=\"Next slide\"\n        disabled={!loop && index >= itemsCount - 1}\n        onClick={handleNextClick}\n      >\n        <NextIcon className=\"stroke-zinc-600 dark:stroke-zinc-50\" size={16} />\n      </button>\n    </div>\n  );\n}\n\nexport type CarouselIndicatorProps = {\n  className?: string;\n  classNameButton?: string;\n};\n\nfunction CarouselIndicator({\n  className,\n  classNameButton,\n}: CarouselIndicatorProps) {\n  const { index, itemsCount, setIndex, orientation } = useCarousel();\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"absolute z-10 flex items-center justify-center\",\n        isVertical\n          ? \"right-0 top-1/2 h-full -translate-y-1/2 flex-col\"\n          : \"bottom-0 w-full\",\n        className,\n      )}\n    >\n      <div\n        className={cn(\"flex\", isVertical ? \"flex-col space-y-2\" : \"space-x-2\")}\n      >\n        {Array.from({ length: itemsCount }, (_, i) => (\n          <button\n            key={i}\n            type=\"button\"\n            aria-label={`Go to slide ${i + 1}`}\n            onClick={() => setIndex(i)}\n            className={cn(\n              \"h-2 w-2 rounded-full transition-opacity duration-300\",\n              index === i\n                ? \"bg-zinc-950 dark:bg-zinc-50\"\n                : \"bg-zinc-900/50 dark:bg-zinc-100/50\",\n              classNameButton,\n            )}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport type CarouselContentProps = {\n  children: ReactNode;\n  className?: string;\n  transition?: {\n    duration?: number;\n    ease?: string;\n  };\n};\n\nfunction CarouselContent({\n  children,\n  className,\n  transition,\n}: CarouselContentProps) {\n  const { index, setIndex, setItemsCount, disableDrag, loop, orientation } =\n    useCarousel();\n  const [visibleItemsCount, setVisibleItemsCount] = useState(1);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const dragStart = useRef<number | null>(null);\n  const isVertical = orientation === \"vertical\";\n\n  const childrenArray = Children.toArray(children);\n  const itemsLength = childrenArray.length;\n\n  // Detect visible items using IntersectionObserver\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const options = {\n      root: containerRef.current,\n      threshold: 0.5,\n    };\n\n    const observer = new IntersectionObserver((entries) => {\n      const visibleCount = entries.filter(\n        (entry) => entry.isIntersecting,\n      ).length;\n      setVisibleItemsCount(visibleCount);\n    }, options);\n\n    const childNodes = containerRef.current.children;\n    Array.from(childNodes).forEach((child) => observer.observe(child));\n\n    return () => observer.disconnect();\n  }, [children]);\n\n  // Calculate max valid index based on visible items\n  const maxIndex = Math.max(0, itemsLength - visibleItemsCount);\n\n  useEffect(() => {\n    if (!itemsLength) {\n      return;\n    }\n\n    // Set page count (number of valid positions), not item count\n    const pageCount = maxIndex + 1;\n    setItemsCount(pageCount);\n  }, [itemsLength, visibleItemsCount, maxIndex, setItemsCount]);\n\n  const handleDragStart = (position: number) => {\n    if (disableDrag) return;\n    dragStart.current = position;\n  };\n\n  const handleDragEnd = (position: number) => {\n    if (disableDrag || dragStart.current === null) return;\n\n    const diff = dragStart.current - position;\n\n    if (diff > 50) {\n      if (index < maxIndex) {\n        setIndex(index + 1);\n      } else if (loop) {\n        setIndex(0);\n      }\n    } else if (diff < -50) {\n      if (index > 0) {\n        setIndex(index - 1);\n      } else if (loop) {\n        setIndex(maxIndex);\n      }\n    }\n\n    dragStart.current = null;\n  };\n\n  const getPosition = (e: React.MouseEvent | React.Touch) => {\n    return isVertical ? e.clientY : e.clientX;\n  };\n\n  const handleMouseDown = (e: React.MouseEvent) => {\n    handleDragStart(getPosition(e));\n  };\n\n  const handleMouseUp = (e: React.MouseEvent) => {\n    handleDragEnd(getPosition(e));\n  };\n\n  const handleMouseLeave = (e: React.MouseEvent) => {\n    if (dragStart.current !== null) {\n      handleDragEnd(getPosition(e));\n    }\n  };\n\n  const handleTouchStart = (e: React.TouchEvent) => {\n    handleDragStart(getPosition(e.touches[0]));\n  };\n\n  const handleTouchEnd = (e: React.TouchEvent) => {\n    handleDragEnd(getPosition(e.changedTouches[0]));\n  };\n\n  const duration = transition?.duration ?? 300;\n  const ease = transition?.ease ?? \"ease-out\";\n\n  const transform = isVertical\n    ? `translateY(-${index * (100 / visibleItemsCount)}%)`\n    : `translateX(-${index * (100 / visibleItemsCount)}%)`;\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"flex h-full\",\n        isVertical ? \"flex-col\" : \"items-center\",\n        !disableDrag && \"cursor-grab active:cursor-grabbing\",\n        className,\n      )}\n      style={{\n        transform,\n        transition: `transform ${duration}ms ${ease}`,\n      }}\n      onMouseDown={handleMouseDown}\n      onMouseUp={handleMouseUp}\n      onMouseLeave={handleMouseLeave}\n      onTouchStart={handleTouchStart}\n      onTouchEnd={handleTouchEnd}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type CarouselItemProps = {\n  children: ReactNode;\n  className?: string;\n};\n\nfunction CarouselItem({ children, className }: CarouselItemProps) {\n  const { orientation } = useCarousel();\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"shrink-0 grow-0 overflow-hidden\",\n        isVertical ? \"h-full min-h-0 w-full\" : \"min-w-0 w-full\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type UseCarouselAutoplayOptions = {\n  interval?: number;\n  autoStart?: boolean;\n};\n\nfunction useCarouselAutoplay(options: UseCarouselAutoplayOptions = {}) {\n  const { interval = 3000, autoStart = true } = options;\n  const { index, setIndex, itemsCount, loop } = useCarousel();\n  const [isPlaying, setIsPlaying] = useState(autoStart);\n\n  useEffect(() => {\n    if (!isPlaying || itemsCount === 0) return;\n\n    const timer = setInterval(() => {\n      if (index < itemsCount - 1) {\n        setIndex(index + 1);\n      } else if (loop) {\n        setIndex(0);\n      } else {\n        setIsPlaying(false);\n      }\n    }, interval);\n\n    return () => clearInterval(timer);\n  }, [index, itemsCount, loop, interval, isPlaying, setIndex]);\n\n  return {\n    isPlaying,\n    play: () => setIsPlaying(true),\n    pause: () => setIsPlaying(false),\n    toggle: () => setIsPlaying((prev) => !prev),\n  };\n}\n\nexport {\n  Carousel,\n  CarouselContent,\n  CarouselIndicator,\n  CarouselItem,\n  CarouselNavigation,\n  useCarousel,\n  useCarouselAutoplay,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
