144 lines
3.7 KiB
TypeScript
144 lines
3.7 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
import Autoplay from "embla-carousel-autoplay"
|
|
import useEmblaCarousel from "embla-carousel-react"
|
|
|
|
interface CarouselApi {
|
|
slideNodes(): HTMLElement[]
|
|
on(event: string, listener: (...args: unknown[]) => void): void
|
|
scrollPrev(): void
|
|
scrollNext(): void
|
|
reInit(): void
|
|
}
|
|
|
|
const CarouselContext = React.createContext<CarouselApi | null>(null)
|
|
|
|
type CarouselProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
opts?: {
|
|
align?: "start" | "center" | "end"
|
|
loop?: boolean
|
|
}
|
|
plugins?: unknown[]
|
|
setApi?: (api: CarouselApi) => void
|
|
}
|
|
|
|
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
({ opts, plugins = [Autoplay()], setApi, className, children, ...props }, ref) => {
|
|
const [api, setApiInternal] = React.useState<CarouselApi | null>(null)
|
|
|
|
const [emblaRef, emblaApi] = useEmblaCarousel(opts, plugins)
|
|
|
|
React.useEffect(() => {
|
|
setApiInternal(emblaApi ?? null)
|
|
}, [emblaApi])
|
|
|
|
React.useEffect(() => {
|
|
if (!api) {
|
|
return
|
|
}
|
|
|
|
setApi?.(api)
|
|
}, [api, setApi])
|
|
|
|
return (
|
|
<CarouselContext.Provider value={api}>
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"relative w-full",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className="overflow-hidden"
|
|
ref={emblaRef}
|
|
style={{ touchAction: 'pan-y pinch-zoom' }}
|
|
>
|
|
<div className="flex">{children}</div>
|
|
</div>
|
|
</div>
|
|
</CarouselContext.Provider>
|
|
)
|
|
}
|
|
)
|
|
Carousel.displayName = "Carousel"
|
|
|
|
interface CarouselContentProps {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
const CarouselContent = React.forwardRef<HTMLDivElement, CarouselContentProps>(
|
|
({ children, className }, ref) => (
|
|
<div ref={ref} className={cn("flex", className)}>
|
|
{children}
|
|
</div>
|
|
)
|
|
)
|
|
CarouselContent.displayName = "CarouselContent"
|
|
|
|
interface CarouselItemProps {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
const CarouselItem = React.forwardRef<HTMLDivElement, CarouselItemProps>(
|
|
({ children, className }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn("min-w-0 shrink-0 grow-0 basis-full pl-4 md:pl-6", className)}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
)
|
|
CarouselItem.displayName = "CarouselItem"
|
|
|
|
const CarouselPrevious = React.forwardRef<
|
|
HTMLButtonElement,
|
|
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<Button
|
|
ref={ref}
|
|
variant="outline"
|
|
size="icon"
|
|
className={cn(
|
|
"absolute -left-1 top-1/2 -translate-y-1/2 rounded-full h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background/90 disabled:pointer-events-none disabled:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span className="sr-only">Previous slide</span>
|
|
</Button>
|
|
))
|
|
CarouselPrevious.displayName = "CarouselPrevious"
|
|
|
|
const CarouselNext = React.forwardRef<
|
|
HTMLButtonElement,
|
|
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<Button
|
|
ref={ref}
|
|
variant="outline"
|
|
size="icon"
|
|
className={cn(
|
|
"absolute -right-1 top-1/2 -translate-y-1/2 rounded-full h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background/90 disabled:pointer-events-none disabled:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ArrowRight className="h-4 w-4" />
|
|
<span className="sr-only">Next slide</span>
|
|
</Button>
|
|
))
|
|
CarouselNext.displayName = "CarouselNext"
|
|
|
|
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }
|