From 71f06de42426095ab65df5d63b427c8015bbcdb9 Mon Sep 17 00:00:00 2001 From: Eric Lay Date: Wed, 11 Mar 2026 09:05:28 -0500 Subject: [PATCH] Add src/components/ui/carousel.jsx --- src/components/ui/carousel.jsx | 193 +++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/components/ui/carousel.jsx diff --git a/src/components/ui/carousel.jsx b/src/components/ui/carousel.jsx new file mode 100644 index 0000000..99ea00e --- /dev/null +++ b/src/components/ui/carousel.jsx @@ -0,0 +1,193 @@ +import * as React from "react" +import useEmblaCarousel from "embla-carousel-react"; +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +const CarouselContext = React.createContext(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(( + { + 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) => { + 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) => { + 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 ( + ( +
+ {children} +
+
) + ); +}) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( + (
+
+
) + ); +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( + (
) + ); +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + () + ); +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + () + ); +}) +CarouselNext.displayName = "CarouselNext" + +export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };