230 lines · 5.4 KB · TypeScript
RawDownload
"use client"; import * as React from "react";import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";import { ArrowLeft, ArrowRight } from "lucide-react"; import { cn } from "../lib/utils";import { Button } from "./button"; type CarouselApi = UseEmblaCarouselType[1];type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;type CarouselOptions = UseCarouselParameters[0];type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = {  opts?: CarouselOptions;  plugins?: CarouselPlugin;  orientation?: "horizontal" | "vertical";  setApi?: (api: CarouselApi) => void;}; type CarouselContextProps = {  carouselRef: ReturnType<typeof useEmblaCarousel>[0];  api: ReturnType<typeof useEmblaCarousel>[1];  scrollPrev: () => void;  scrollNext: () => void;  canScrollPrev: boolean;  canScrollNext: boolean;} & CarouselProps; const CarouselContext = React.createContext<CarouselContextProps | null>(null); function useCarousel() {  const context = React.useContext(CarouselContext);   if (!context) {    throw new Error("useCarousel must be used within a <Carousel />");  }   return context;} function Carousel({  orientation = "horizontal",  opts,  setApi,  plugins,  className,  children,  ...props}: React.ComponentProps<"div"> & CarouselProps) {  const [carouselRef, api] = useEmblaCarousel(    {      ...opts,      axis: orientation === "horizontal" ? "x" : "y",    },    plugins,  );  const [canScrollPrev, setCanScrollPrev] = React.useState(false);  const [canScrollNext, setCanScrollNext] = React.useState(false);   const onSelect = React.useCallback((api: CarouselApi) => {    if (!api) return;    setCanScrollPrev(api.canScrollPrev());    setCanScrollNext(api.canScrollNext());  }, []);   const scrollPrev = React.useCallback(() => {    api?.scrollPrev();  }, [api]);   const scrollNext = React.useCallback(() => {    api?.scrollNext();  }, [api]);   const handleKeyDown = React.useCallback(    (event: React.KeyboardEvent<HTMLDivElement>) => {      if (event.key === "ArrowLeft") {        event.preventDefault();        scrollPrev();      } else if (event.key === "ArrowRight") {        event.preventDefault();        scrollNext();      }    },    [scrollPrev, scrollNext],  );   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("select", onSelect);    };  }, [api, onSelect]);   return (    <CarouselContext.Provider      value={{        carouselRef,        api: api,        opts,        orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),        scrollPrev,        scrollNext,        canScrollPrev,        canScrollNext,      }}    >      <div        onKeyDownCapture={handleKeyDown}        className={cn("relative", className)}        role="region"        aria-roledescription="carousel"        data-slot="carousel"        {...props}      >        {children}      </div>    </CarouselContext.Provider>  );} function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {  const { carouselRef, orientation } = useCarousel();   return (    <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">      <div        className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}        {...props}      />    </div>  );} function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {  const { orientation } = useCarousel();   return (    <div      role="group"      aria-roledescription="slide"      data-slot="carousel-item"      className={cn(        "min-w-0 shrink-0 grow-0 basis-full",        orientation === "horizontal" ? "pl-4" : "pt-4",        className,      )}      {...props}    />  );} function CarouselPrevious({  className,  variant = "outline",  size = "icon",  ...props}: React.ComponentProps<typeof Button>) {  const { orientation, scrollPrev, canScrollPrev } = useCarousel();   return (    <Button      data-slot="carousel-previous"      variant={variant}      size={size}      className={cn(        "absolute size-8 rounded-full",        orientation === "horizontal"          ? "top-1/2 -left-12 -translate-y-1/2"          : "-top-12 left-1/2 -translate-x-1/2 rotate-90",        className,      )}      disabled={!canScrollPrev}      onClick={scrollPrev}      {...props}    >      <ArrowLeft />      <span className="sr-only">Previous slide</span>    </Button>  );} function CarouselNext({  className,  variant = "outline",  size = "icon",  ...props}: React.ComponentProps<typeof Button>) {  const { orientation, scrollNext, canScrollNext } = useCarousel();   return (    <Button      data-slot="carousel-next"      variant={variant}      size={size}      className={cn(        "absolute size-8 rounded-full",        orientation === "horizontal"          ? "top-1/2 -right-12 -translate-y-1/2"          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",        className,      )}      disabled={!canScrollNext}      onClick={scrollNext}      {...props}    >      <ArrowRight />      <span className="sr-only">Next slide</span>    </Button>  );} export {  type CarouselApi,  Carousel,  CarouselContent,  CarouselItem,  CarouselPrevious,  CarouselNext,};