123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- "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
- }
- 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 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<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"
- export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
- }
|