Last active
December 31, 2023 02:18
-
-
Save superbahbi/85ee6ba70f2e0b0a30222b709e85fc44 to your computer and use it in GitHub Desktop.
Revisions
-
superbahbi renamed this gist
Dec 31, 2023 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
superbahbi created this gist
Dec 31, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,340 @@ import useEmblaCarousel, { type EmblaCarouselType as CarouselApi, type EmblaOptionsType as CarouselOptions, type EmblaPluginType as CarouselPlugin, } from "embla-carousel-react"; import { ArrowLeft, ArrowRight } from "lucide-react"; import * as React from "react"; import { Button, ButtonProps } from "@/components/ui/button"; import { cn } from "@/lib/utils"; 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; scrollTo: (index: number) => void; selectedIndex: number; } & 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; } const Carousel = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & 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 [selectedIndex, setSelectedIndex] = React.useState(0); const onSelect = React.useCallback((api: CarouselApi) => { if (!api) { return; } setCanScrollPrev(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); setSelectedIndex(api.selectedScrollSnap()); }, []); 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]); const scrollTo = React.useCallback( (index: number) => api && api.scrollTo(index), [api], ); return ( <CarouselContext.Provider value={{ carouselRef, api: api, opts, orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), scrollPrev, scrollNext, canScrollPrev, canScrollNext, scrollTo, selectedIndex, }} > <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<HTMLDivElement> >(({ 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<HTMLDivElement> >(({ 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<typeof Button> >(({ 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<typeof Button> >(({ 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"; const DotSvg = ({ isSelected }: { isSelected: boolean }) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className={cn( isSelected ? " fill-blue-500 text-blue-500" : "fill-slate-400 text-slate-400", "h-5 w-5", )} > <circle cx="12.1" cy="12.1" r="5" /> </svg> ); type DotButtonProps = ButtonProps & { isSelected: boolean; onClick: () => void; }; const DotButton = React.forwardRef<HTMLButtonElement, DotButtonProps>( ({ className, isSelected, onClick, ...props }, ref) => { return ( <button className={cn(isSelected ? " fill-blue-500" : "fill-slate-400")} ref={ref} onClick={onClick} {...props} > <DotSvg isSelected={isSelected} /> </button> ); }, ); DotButton.displayName = "DotButton"; const CarouselNavigation = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >((props, ref) => { const { api, selectedIndex, scrollTo } = useCarousel(); return ( <div ref={ref} className={cn("flex items-center justify-center")} {...props} > {api && api .scrollSnapList() .map((_, index) => ( <DotButton key={index} isSelected={index === selectedIndex} onClick={() => scrollTo(index)} /> ))} </div> ); }); CarouselNavigation.displayName = "CarouselNavigation"; export { Carousel, CarouselContent, CarouselItem, CarouselNavigation, CarouselNext, CarouselPrevious, type CarouselApi, };