"use client" import * as React from "react" import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" export type CarouselProps = React.ComponentProps<"div"> & { index?: number defaultIndex?: number onIndexChange?: (index: number) => void loop?: boolean variant?: "default" | "hero" | "minimal" showDots?: boolean showArrows?: boolean keyboard?: boolean ariaLabel?: string previousLabel?: string nextLabel?: string swipeThreshold?: number autoplay?: boolean autoplayInterval?: number pauseOnHover?: boolean stopAutoplayOnInteraction?: boolean showPlaybackControl?: boolean showStatus?: boolean showThumbnails?: boolean playLabel?: string pauseLabel?: string statusLabel?: (index: number, total: number) => string renderThumbnail?: (item: React.ReactNode, index: number, active: boolean) => React.ReactNode renderActiveDetail?: (item: React.ReactNode, index: number) => React.ReactNode aspectRatio?: string mouseDrag?: boolean onAutoplayChange?: (playing: boolean) => void viewportClassName?: string controlsClassName?: string dotsClassName?: string arrowClassName?: string dotClassName?: string activeDotClassName?: string thumbnailsClassName?: string } export type CarouselItemProps = React.ComponentProps<"div"> function clampIndex(index: number, length: number, loop: boolean) { if (length <= 0) return 0 if (loop) return ((index % length) + length) % length return Math.min(Math.max(index, 0), length - 1) } function Carousel({ index, defaultIndex = 0, onIndexChange, loop = false, variant = "default", showDots = true, showArrows = true, keyboard = true, ariaLabel = "Carousel", previousLabel = "Previous slide", nextLabel = "Next slide", swipeThreshold = 44, autoplay = false, autoplayInterval = 4500, pauseOnHover = true, stopAutoplayOnInteraction = true, showPlaybackControl = false, showStatus = false, showThumbnails = false, playLabel = "Start autoplay", pauseLabel = "Pause autoplay", statusLabel, renderThumbnail, renderActiveDetail, aspectRatio, mouseDrag = true, onAutoplayChange, viewportClassName, controlsClassName, dotsClassName, arrowClassName, dotClassName, activeDotClassName, thumbnailsClassName, className, children, ...props }: CarouselProps) { const items = React.Children.toArray(children) const [internalIndex, setInternalIndex] = React.useState(defaultIndex) const touchStartXRef = React.useRef(null) const pointerStartXRef = React.useRef(null) const [isHovered, setIsHovered] = React.useState(false) const [autoplayStopped, setAutoplayStopped] = React.useState(false) const [autoplayEnabled, setAutoplayEnabled] = React.useState(autoplay) const controlled = index !== undefined const itemCount = items.length const activeIndex = clampIndex(controlled ? index : internalIndex, itemCount, loop) const activeItem = items[activeIndex] React.useEffect(() => { setAutoplayEnabled(autoplay) }, [autoplay]) const setActiveIndex = React.useCallback((nextIndex: number, reason: "manual" | "autoplay" = "manual") => { const resolvedIndex = clampIndex(nextIndex, itemCount, loop) if (!controlled) setInternalIndex(resolvedIndex) if (reason === "manual" && stopAutoplayOnInteraction) { setAutoplayStopped(true) if (autoplayEnabled) { setAutoplayEnabled(false) onAutoplayChange?.(false) } } onIndexChange?.(resolvedIndex) }, [ autoplayEnabled, controlled, itemCount, loop, onAutoplayChange, onIndexChange, stopAutoplayOnInteraction, ]) const canGoPrevious = loop || activeIndex > 0 const canGoNext = loop || activeIndex < itemCount - 1 const handleTouchStart: React.TouchEventHandler = (event) => { touchStartXRef.current = event.touches[0]?.clientX ?? null } const handleTouchEnd: React.TouchEventHandler = (event) => { if (touchStartXRef.current == null || items.length <= 1) return const endX = event.changedTouches[0]?.clientX ?? touchStartXRef.current const deltaX = endX - touchStartXRef.current touchStartXRef.current = null if (Math.abs(deltaX) < swipeThreshold) return if (deltaX > 0 && canGoPrevious) setActiveIndex(activeIndex - 1) if (deltaX < 0 && canGoNext) setActiveIndex(activeIndex + 1) } const handlePointerDown: React.PointerEventHandler = (event) => { if (!mouseDrag || event.pointerType === "touch") return pointerStartXRef.current = event.clientX } const handlePointerUp: React.PointerEventHandler = (event) => { if (!mouseDrag || pointerStartXRef.current == null || items.length <= 1) return const deltaX = event.clientX - pointerStartXRef.current pointerStartXRef.current = null if (Math.abs(deltaX) < swipeThreshold) return if (deltaX > 0 && canGoPrevious) setActiveIndex(activeIndex - 1) if (deltaX < 0 && canGoNext) setActiveIndex(activeIndex + 1) } React.useEffect(() => { if (!autoplayEnabled || itemCount <= 1 || autoplayStopped) return if (pauseOnHover && isHovered) return const timer = window.setInterval(() => { if (!loop && activeIndex >= itemCount - 1) { setActiveIndex(0, "autoplay") return } setActiveIndex(activeIndex + 1, "autoplay") }, autoplayInterval) return () => { window.clearInterval(timer) } }, [activeIndex, autoplayEnabled, autoplayInterval, autoplayStopped, isHovered, itemCount, loop, pauseOnHover, setActiveIndex]) return (
{ if (!keyboard || itemCount <= 1) return if (event.key === "ArrowLeft" && canGoPrevious) { event.preventDefault() setActiveIndex(activeIndex - 1) } if (event.key === "ArrowRight" && canGoNext) { event.preventDefault() setActiveIndex(activeIndex + 1) } if (event.key === "Home") { event.preventDefault() setActiveIndex(0) } if (event.key === "End") { event.preventDefault() setActiveIndex(itemCount - 1) } }} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} onPointerDown={handlePointerDown} onPointerUp={handlePointerUp} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} {...props} >
{showStatus ? (
{statusLabel?.(activeIndex, items.length) ?? `Slide ${activeIndex + 1} / ${items.length}`}
) : null}
{items[activeIndex]}
{showArrows && items.length > 1 ? ( <> ) : null}
{showDots && items.length > 1 && (
{items.map((_, itemIndex) => ( ) : null}
)} {showThumbnails && items.length > 1 ? (
{items.map((item, itemIndex) => { const active = itemIndex === activeIndex return ( ) })}
) : null} {renderActiveDetail ? renderActiveDetail(activeItem, activeIndex) : null} {statusLabel?.(activeIndex, items.length) ?? `${ariaLabel}: slide ${activeIndex + 1} of ${items.length}`}
) } function CarouselItem({ className, ...props }: CarouselItemProps) { return
} export { Carousel, CarouselItem }