Files
a37f030d-6e8a-4f3d-9f23-1ce…/research/lovable/carousel.md
vitalijmulika fb5d40bc69 Initial commit
2026-04-29 13:20:41 +03:00

6.1 KiB

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 "@/components/ui/button";

type CarouselApi = UseEmblaCarouselType[1]; type UseCarouselParameters = Parameters; 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[0]; api: ReturnType[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 "); }

return context; }

const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes & CarouselProps>( ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => { 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
      ref={ref}
      onKeyDownCapture={handleKeyDown}
      className={cn("relative", className)}
      role="region"
      aria-roledescription="carousel"
      {...props}
    >
      {children}
    </div>
  </CarouselContext.Provider>
);

}, ); Carousel.displayName = "Carousel";

const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes>( ({ className, ...props }, ref) => { const { carouselRef, orientation } = useCarousel();

return (
  <div ref={carouselRef} className="overflow-hidden">
    <div
      ref={ref}
      className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
      {...props}
    />
  </div>
);

}, ); CarouselContent.displayName = "CarouselContent";

const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes>( ({ className, ...props }, ref) => { const { orientation } = useCarousel();

return (
  <div
    ref={ref}
    role="group"
    aria-roledescription="slide"
    className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
    {...props}
  />
);

}, ); CarouselItem.displayName = "CarouselItem";

const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps>( ({ className, variant = "outline", size = "icon", ...props }, ref) => { const { orientation, scrollPrev, canScrollPrev } = useCarousel();

return (
  <Button
    ref={ref}
    variant={variant}
    size={size}
    className={cn(
      "absolute h-8 w-8 rounded-full",
      orientation === "horizontal"
        ? "-left-12 top-1/2 -translate-y-1/2"
        : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
      className,
    )}
    disabled={!canScrollPrev}
    onClick={scrollPrev}
    {...props}
  >
    <ArrowLeft className="h-4 w-4" />
    <span className="sr-only">Previous slide</span>
  </Button>
);

}, ); CarouselPrevious.displayName = "CarouselPrevious";

const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps>( ({ className, variant = "outline", size = "icon", ...props }, ref) => { const { orientation, scrollNext, canScrollNext } = useCarousel();

return (
  <Button
    ref={ref}
    variant={variant}
    size={size}
    className={cn(
      "absolute h-8 w-8 rounded-full",
      orientation === "horizontal"
        ? "-right-12 top-1/2 -translate-y-1/2"
        : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
      className,
    )}
    disabled={!canScrollNext}
    onClick={scrollNext}
    {...props}
  >
    <ArrowRight className="h-4 w-4" />
    <span className="sr-only">Next slide</span>
  </Button>
);

}, ); CarouselNext.displayName = "CarouselNext";

export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };