carousel.tsx raw

   1  "use client";
   2  
   3  import * as React from "react";
   4  import useEmblaCarousel, {
   5    type UseEmblaCarouselType,
   6  } from "embla-carousel-react";
   7  import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
   8  
   9  import { cn } from "src/lib/utils";
  10  import { Button } from "src/components/ui/button";
  11  
  12  type CarouselApi = UseEmblaCarouselType[1];
  13  type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
  14  type CarouselOptions = UseCarouselParameters[0];
  15  type CarouselPlugin = UseCarouselParameters[1];
  16  
  17  type CarouselProps = {
  18    opts?: CarouselOptions;
  19    plugins?: CarouselPlugin;
  20    orientation?: "horizontal" | "vertical";
  21    setApi?: (api: CarouselApi) => void;
  22  };
  23  
  24  type CarouselContextProps = {
  25    carouselRef: ReturnType<typeof useEmblaCarousel>[0];
  26    api: ReturnType<typeof useEmblaCarousel>[1];
  27    scrollPrev: () => void;
  28    scrollNext: () => void;
  29    canScrollPrev: boolean;
  30    canScrollNext: boolean;
  31  } & CarouselProps;
  32  
  33  const CarouselContext = React.createContext<CarouselContextProps | null>(null);
  34  
  35  function useCarousel() {
  36    const context = React.useContext(CarouselContext);
  37  
  38    if (!context) {
  39      throw new Error("useCarousel must be used within a <Carousel />");
  40    }
  41  
  42    return context;
  43  }
  44  
  45  function Carousel({
  46    orientation = "horizontal",
  47    opts,
  48    setApi,
  49    plugins,
  50    className,
  51    children,
  52    ...props
  53  }: React.ComponentProps<"div"> & CarouselProps) {
  54    const [carouselRef, api] = useEmblaCarousel(
  55      {
  56        ...opts,
  57        axis: orientation === "horizontal" ? "x" : "y",
  58      },
  59      plugins
  60    );
  61    const [canScrollPrev, setCanScrollPrev] = React.useState(false);
  62    const [canScrollNext, setCanScrollNext] = React.useState(false);
  63  
  64    const onSelect = React.useCallback((api: CarouselApi) => {
  65      if (!api) {
  66        return;
  67      }
  68      setCanScrollPrev(api.canScrollPrev());
  69      setCanScrollNext(api.canScrollNext());
  70    }, []);
  71  
  72    const scrollPrev = React.useCallback(() => {
  73      api?.scrollPrev();
  74    }, [api]);
  75  
  76    const scrollNext = React.useCallback(() => {
  77      api?.scrollNext();
  78    }, [api]);
  79  
  80    const handleKeyDown = React.useCallback(
  81      (event: React.KeyboardEvent<HTMLDivElement>) => {
  82        if (event.key === "ArrowLeft") {
  83          event.preventDefault();
  84          scrollPrev();
  85        } else if (event.key === "ArrowRight") {
  86          event.preventDefault();
  87          scrollNext();
  88        }
  89      },
  90      [scrollPrev, scrollNext]
  91    );
  92  
  93    React.useEffect(() => {
  94      if (!api || !setApi) {
  95        return;
  96      }
  97      setApi(api);
  98    }, [api, setApi]);
  99  
 100    React.useEffect(() => {
 101      if (!api) {
 102        return;
 103      }
 104      onSelect(api);
 105      api.on("reInit", onSelect);
 106      api.on("select", onSelect);
 107  
 108      return () => {
 109        api?.off("select", onSelect);
 110      };
 111    }, [api, onSelect]);
 112  
 113    return (
 114      <CarouselContext.Provider
 115        value={{
 116          carouselRef,
 117          api: api,
 118          opts,
 119          orientation:
 120            orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
 121          scrollPrev,
 122          scrollNext,
 123          canScrollPrev,
 124          canScrollNext,
 125        }}
 126      >
 127        <div
 128          onKeyDownCapture={handleKeyDown}
 129          className={cn("relative", className)}
 130          role="region"
 131          aria-roledescription="carousel"
 132          data-slot="carousel"
 133          {...props}
 134        >
 135          {children}
 136        </div>
 137      </CarouselContext.Provider>
 138    );
 139  }
 140  
 141  function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
 142    const { carouselRef, orientation } = useCarousel();
 143  
 144    return (
 145      <div
 146        ref={carouselRef}
 147        className="overflow-hidden"
 148        data-slot="carousel-content"
 149      >
 150        <div
 151          className={cn(
 152            "flex",
 153            orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
 154            className
 155          )}
 156          {...props}
 157        />
 158      </div>
 159    );
 160  }
 161  
 162  function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
 163    const { orientation } = useCarousel();
 164  
 165    return (
 166      <div
 167        role="group"
 168        aria-roledescription="slide"
 169        data-slot="carousel-item"
 170        className={cn(
 171          "min-w-0 shrink-0 grow-0 basis-full",
 172          orientation === "horizontal" ? "pl-4" : "pt-4",
 173          className
 174        )}
 175        {...props}
 176      />
 177    );
 178  }
 179  
 180  function CarouselPrevious({
 181    className,
 182    variant = "outline",
 183    size = "icon",
 184    ...props
 185  }: React.ComponentProps<typeof Button>) {
 186    const { orientation, scrollPrev, canScrollPrev } = useCarousel();
 187  
 188    return (
 189      <Button
 190        data-slot="carousel-previous"
 191        variant={variant}
 192        size={size}
 193        className={cn(
 194          "absolute size-8 rounded-full",
 195          orientation === "horizontal"
 196            ? "top-1/2 -left-12 -translate-y-1/2"
 197            : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
 198          className
 199        )}
 200        disabled={!canScrollPrev}
 201        onClick={scrollPrev}
 202        {...props}
 203      >
 204        <ArrowLeftIcon />
 205        <span className="sr-only">Previous slide</span>
 206      </Button>
 207    );
 208  }
 209  
 210  function CarouselNext({
 211    className,
 212    variant = "outline",
 213    size = "icon",
 214    ...props
 215  }: React.ComponentProps<typeof Button>) {
 216    const { orientation, scrollNext, canScrollNext } = useCarousel();
 217  
 218    return (
 219      <Button
 220        data-slot="carousel-next"
 221        variant={variant}
 222        size={size}
 223        className={cn(
 224          "absolute size-8 rounded-full",
 225          orientation === "horizontal"
 226            ? "top-1/2 -right-12 -translate-y-1/2"
 227            : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
 228          className
 229        )}
 230        disabled={!canScrollNext}
 231        onClick={scrollNext}
 232        {...props}
 233      >
 234        <ArrowRightIcon />
 235        <span className="sr-only">Next slide</span>
 236      </Button>
 237    );
 238  }
 239  
 240  export {
 241    type CarouselApi,
 242    Carousel,
 243    CarouselContent,
 244    CarouselItem,
 245    CarouselPrevious,
 246    CarouselNext,
 247  };
 248