Spaces:
Running
Running
"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 "@/components/ui/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, | |
} | |