"use client"; import {mergeProps} from "@base-ui/react/merge-props"; import useEmblaCarousel, {type UseEmblaCarouselType} from "embla-carousel-react"; import {ArrowLeft, ArrowRight} from "lucide-react"; import * as React from "react"; import {Button} from "@/components/ui/button"; import {cn} from "@/lib/utilities"; import styles from "./carousel.module.css"; type CarouselApi = NonNullable; type UseCarouselParameters = Parameters; type CarouselOptions = UseCarouselParameters[0]; type CarouselPlugin = UseCarouselParameters[1]; /** * Carousel configuration props. */ type CarouselProps = { /** * Embla carousel options forwarded to the underlying carousel instance. * @default undefined */ opts?: CarouselOptions; /** * Embla plugins applied to the carousel instance. * @default undefined */ plugins?: CarouselPlugin; /** * Axis orientation used for layout and keyboard navigation. This prop also * controls Embla's internal `axis` option (`"x"` for horizontal and `"y"` * for vertical). * @default "horizontal" */ orientation?: "horizontal" | "vertical"; /** * Callback invoked with the initialized Embla API instance. * @default undefined */ setApi?: (api: CarouselApi) => void; }; type CarouselContextProps = { carouselRef: ReturnType[0]; api: ReturnType[1]; scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean; } & CarouselProps; const CarouselContext = React.createContext(null); function useCarousel(): CarouselContextProps { const context = React.useContext(CarouselContext); if (!context) { throw new Error("useCarousel must be used within a "); } return context; } /** * Provides a compound carousel container powered by Embla. * * @remarks * - Renders a `
` element * - Built on `embla-carousel-react` * - Exposes context for content, items, and navigation controls * * @example * ```tsx * * * Slide 1 * Slide 2 * * * * * ``` * * @param props.opts - Embla options forwarded to the carousel instance. Common * options include `loop` for infinite scrolling, `align` for slide alignment, * `slidesToScroll` to control navigation increments, `dragFree` for momentum * dragging, `duration` for transition timing, and `startIndex` for the initial * slide. * @param props.plugins - Embla plugins applied to the carousel instance. * * @example With autoplay * ```tsx * import Autoplay from "embla-carousel-autoplay"; * * * ... * * ``` * * @param props.setApi - Optional callback for advanced Embla API access after * initialization, useful for custom controls, analytics, or external state * synchronization. * * @see {@link https://www.embla-carousel.com/get-started/react/ | Embla React Docs} */ const Carousel = React.forwardRef & CarouselProps>( ({orientation, opts, setApi, plugins, className, children, ...props}, ref) => { // Derive axis from orientation, or orientation from opts.axis, defaulting to horizontal. const resolvedOrientation = orientation ?? (opts?.axis === "y" ? "vertical" : "horizontal"); const resolvedAxis = resolvedOrientation === "vertical" ? "y" : "x"; const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: resolvedAxis, }, plugins, ); const [canScrollPrev, setCanScrollPrev] = React.useState(false); const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((emblaApi: CarouselApi) => { if (!emblaApi) { return; } // eslint-disable-next-line react-x/set-state-in-effect -- syncing embla scroll capability into state is the effect's purpose setCanScrollPrev(emblaApi.canScrollPrev()); // eslint-disable-next-line react-x/set-state-in-effect -- syncing embla scroll capability into state is the effect's purpose setCanScrollNext(emblaApi.canScrollNext()); }, []); const scrollPrev = React.useCallback(() => { api?.scrollPrev(); }, [api]); const scrollNext = React.useCallback(() => { api?.scrollNext(); }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (orientation === "horizontal") { if (event.key === "ArrowLeft") { event.preventDefault(); scrollPrev(); } else if (event.key === "ArrowRight") { event.preventDefault(); scrollNext(); } return; } if (event.key === "ArrowUp") { event.preventDefault(); scrollPrev(); } else if (event.key === "ArrowDown") { event.preventDefault(); scrollNext(); } }, [orientation, scrollNext, scrollPrev], ); React.useEffect(() => { if (!api || !setApi) { return; } setApi(api); }, [api, setApi]); React.useEffect(() => { if (!api) { return; } onSelect(api); api.on("reInit", onSelect); api.on("select", onSelect); return () => { api.off("reInit", onSelect); api.off("select", onSelect); }; }, [api, onSelect]); return (
{children}
); }, ); Carousel.displayName = "Carousel"; /** * Renders the scrollable track that contains carousel slides. * * @remarks * - Renders nested `
` elements * - Built on Embla's viewport and track structure * * @example * ```tsx * * Slide * * ``` * * @see {@link https://www.embla-carousel.com/get-started/react/ | Embla React Docs} */ const CarouselContent = React.forwardRef>(({className, ...props}, ref) => { const {carouselRef, orientation} = useCarousel(); return (
); }); CarouselContent.displayName = "CarouselContent"; /** * Renders an individual carousel slide. * * @remarks * - Renders a `
` element * - Built on the shared carousel context for orientation-aware styling * * @example * ```tsx * Slide content * ``` * * @see {@link https://www.embla-carousel.com/get-started/react/ | Embla React Docs} */ const CarouselItem = React.forwardRef>(({className, ...props}, ref) => { const {orientation} = useCarousel(); return (
); }); CarouselItem.displayName = "CarouselItem"; /** * Renders the previous-slide navigation button. * * @remarks * - Renders the shared ` ); }, ); CarouselPrevious.displayName = "CarouselPrevious"; /** * Renders the next-slide navigation button. * * @remarks * - Renders the shared ` ); }, ); CarouselNext.displayName = "CarouselNext"; export {Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi}; export type {CarouselOptions, CarouselPlugin, CarouselProps};