"use client"; import { cn } from "@/app/lib/cn"; import useEmblaCarousel from "embla-carousel-react"; import { ArrowLeft, ArrowRight } from "iconsax-reactjs"; import { createContext, useContext, useEffect, useState, type FC, type HTMLAttributes, type ReactNode, } from "react"; type CarouselApi = { scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean; slidesPerView: number; gap: number; }; type ViewportRef = (node: HTMLElement | null) => void; const CarouselContext = createContext(null); const CarouselViewportContext = createContext(null); function useCarousel() { const context = useContext(CarouselContext); if (!context) { throw new Error("Carousel components must be used within "); } return context; } type ResponsiveNumber = number | { base: number; md?: number; lg?: number }; function useResponsiveNumber(value: ResponsiveNumber): number { const base = typeof value === "number" ? value : value.base; const md = typeof value === "number" ? undefined : value.md; const lg = typeof value === "number" ? undefined : value.lg; const resolve = () => { if (typeof window === "undefined") return base; if (window.matchMedia("(min-width: 1024px)").matches) return lg ?? md ?? base; if (window.matchMedia("(min-width: 768px)").matches) return md ?? base; return base; }; const [resolved, setResolved] = useState(base); useEffect(() => { const update = () => setResolved(resolve()); update(); window.addEventListener("resize", update); return () => window.removeEventListener("resize", update); }, [base, md, lg]); return resolved; } type CarouselProps = { children: ReactNode; slidesPerView?: ResponsiveNumber; slidesToScroll?: number; gap?: ResponsiveNumber; direction?: "rtl" | "ltr"; className?: string; }; const Carousel: FC = ({ children, slidesPerView = 5, slidesToScroll, gap = 40, direction = "rtl", className, }) => { const resolvedSlidesPerView = useResponsiveNumber(slidesPerView); const resolvedGap = useResponsiveNumber(gap); const scrollBy = slidesToScroll ?? Math.max(1, Math.floor(resolvedSlidesPerView)); const [emblaRef, emblaApi] = useEmblaCarousel({ align: "start", direction, slidesToScroll: scrollBy, containScroll: "trimSnaps", }); const [canScrollPrev, setCanScrollPrev] = useState(false); const [canScrollNext, setCanScrollNext] = useState(false); useEffect(() => { if (!emblaApi) return; const updateButtons = () => { setCanScrollPrev(emblaApi.canScrollPrev()); setCanScrollNext(emblaApi.canScrollNext()); }; updateButtons(); emblaApi.on("select", updateButtons); emblaApi.on("reInit", updateButtons); return () => { emblaApi.off("select", updateButtons); emblaApi.off("reInit", updateButtons); }; }, [emblaApi]); useEffect(() => { emblaApi?.reInit({ slidesToScroll: scrollBy }); }, [emblaApi, scrollBy, resolvedSlidesPerView, resolvedGap]); return ( emblaApi?.scrollPrev(), scrollNext: () => emblaApi?.scrollNext(), canScrollPrev, canScrollNext, slidesPerView: resolvedSlidesPerView, gap: resolvedGap, }} >
{children}
); }; type CarouselControlsProps = { className?: string; prevLabel?: string; nextLabel?: string; }; const CarouselControls: FC = ({ className, prevLabel = "قبلی", nextLabel = "بعدی", }) => { const { scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel(); return (
); }; type CarouselTrackProps = { children: ReactNode; className?: string; }; const CarouselTrack: FC = ({ children, className }) => { const emblaRef = useContext(CarouselViewportContext); const { gap } = useCarousel(); if (!emblaRef) { throw new Error("CarouselTrack must be used within "); } return (
{children}
); }; type CarouselSlideProps = HTMLAttributes & { children: ReactNode; }; const CarouselSlide: FC = ({ children, className, style, ...rest }) => { const { slidesPerView, gap } = useCarousel(); const totalGap = (slidesPerView - 1) * gap; return (
{children}
); }; export { Carousel, CarouselControls, CarouselSlide, CarouselTrack }; export default Carousel;