8a9f5ddc7d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
5.8 KiB
TypeScript
243 lines
5.8 KiB
TypeScript
"use client";
|
||
|
||
import * as React from "react";
|
||
import { cn } from "cn";
|
||
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||
|
||
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;
|
||
// İlk durum bir sonraki karede okunur (effect içinde senkron setState yapılmaz).
|
||
const frame = requestAnimationFrame(() => onSelect(api));
|
||
api.on("reInit", onSelect);
|
||
api.on("select", onSelect);
|
||
|
||
return () => {
|
||
cancelAnimationFrame(frame);
|
||
api.off("select", onSelect);
|
||
api.off("reInit", 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,
|
||
containerClassName,
|
||
...props
|
||
}: React.ComponentProps<"div"> & { containerClassName?: string }) {
|
||
const { carouselRef, orientation } = useCarousel();
|
||
|
||
return (
|
||
<div
|
||
ref={carouselRef}
|
||
className={cn("overflow-hidden", containerClassName)}
|
||
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-sm",
|
||
...props
|
||
}: React.ComponentProps<typeof Button>) {
|
||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||
|
||
return (
|
||
<Button
|
||
data-slot="carousel-previous"
|
||
variant={variant}
|
||
size={size}
|
||
className={cn(
|
||
"absolute touch-manipulation rounded-full",
|
||
orientation === "horizontal"
|
||
? "inset-y-0 -left-12 my-auto"
|
||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||
className,
|
||
)}
|
||
disabled={!canScrollPrev}
|
||
onClick={scrollPrev}
|
||
{...props}
|
||
>
|
||
<ChevronLeftIcon />
|
||
<span className="sr-only">Previous slide</span>
|
||
</Button>
|
||
);
|
||
}
|
||
|
||
function CarouselNext({
|
||
className,
|
||
variant = "outline",
|
||
size = "icon-sm",
|
||
...props
|
||
}: React.ComponentProps<typeof Button>) {
|
||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||
|
||
return (
|
||
<Button
|
||
data-slot="carousel-next"
|
||
variant={variant}
|
||
size={size}
|
||
className={cn(
|
||
"absolute touch-manipulation rounded-full",
|
||
orientation === "horizontal"
|
||
? "inset-y-0 -right-12 my-auto"
|
||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||
className,
|
||
)}
|
||
disabled={!canScrollNext}
|
||
onClick={scrollNext}
|
||
{...props}
|
||
>
|
||
<ChevronRightIcon />
|
||
<span className="sr-only">Next slide</span>
|
||
</Button>
|
||
);
|
||
}
|
||
|
||
export {
|
||
type CarouselApi,
|
||
Carousel,
|
||
CarouselContent,
|
||
CarouselItem,
|
||
CarouselPrevious,
|
||
CarouselNext,
|
||
useCarousel,
|
||
};
|