"use client";
// Visit https://kaif-ui.vercel.app/ for more components like this
import {
Children,
ReactNode,
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { motion, Transition, useMotionValue } from "framer-motion";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
type CarouselContextType = {
index: number;
setIndex: (newIndex: number) => void;
itemsCount: number;
setItemsCount: (newItemsCount: number) => void;
disableDrag: boolean;
};
const CarouselContext = createContext<CarouselContextType | undefined>(
undefined
);
function useCarousel() {
const context = useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within an CarouselProvider");
}
return context;
}
type CarouselProviderProps = {
children: ReactNode;
initialIndex?: number;
onIndexChange?: (newIndex: number) => void;
disableDrag?: boolean;
};
function CarouselProvider({
children,
initialIndex = 0,
onIndexChange,
disableDrag = false,
}: CarouselProviderProps) {
const [index, setIndex] = useState<number>(initialIndex);
const [itemsCount, setItemsCount] = useState<number>(0);
const handleSetIndex = (newIndex: number) => {
setIndex(newIndex);
onIndexChange?.(newIndex);
};
useEffect(() => {
setIndex(initialIndex);
}, [initialIndex]);
return (
<CarouselContext.Provider
value={{
index,
setIndex: handleSetIndex,
itemsCount,
setItemsCount,
disableDrag,
}}
>
{children}
</CarouselContext.Provider>
);
}
type CarouselProps = {
children: ReactNode;
className?: string;
initialIndex?: number;
index?: number;
onIndexChange?: (newIndex: number) => void;
disableDrag?: boolean;
};
function Carousel({
children,
className,
initialIndex = 0,
index: externalIndex,
onIndexChange,
disableDrag = false,
}: CarouselProps) {
const [internalIndex, setInternalIndex] = useState<number>(initialIndex);
const isControlled = externalIndex !== undefined;
const currentIndex = isControlled ? externalIndex : internalIndex;
const handleIndexChange = (newIndex: number) => {
if (!isControlled) {
setInternalIndex(newIndex);
}
onIndexChange?.(newIndex);
};
return (
<CarouselProvider
initialIndex={currentIndex}
onIndexChange={handleIndexChange}
disableDrag={disableDrag}
>
<div className={cn("group/hover relative", className)}>
<div className="overflow-hidden">{children}</div>
</div>
</CarouselProvider>
);
}
type CarouselNavigationProps = {
className?: string;
classNameButton?: string;
alwaysShow?: boolean;
};
function CarouselNavigation({
className,
classNameButton,
alwaysShow,
}: CarouselNavigationProps) {
const { index, setIndex, itemsCount } = useCarousel();
return (
<div
className={cn(
"pointer-events-none absolute left-[-12.5%] top-1/2 flex w-[125%] -translate-y-1/2 justify-between px-2",
className
)}
>
<button
type="button"
className={cn(
"pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950",
alwaysShow
? "opacity-100"
: "opacity-0 group-hover/hover:opacity-100",
alwaysShow
? "disabled:opacity-40"
: "disabled:group-hover/hover:opacity-40",
classNameButton
)}
disabled={index === 0}
onClick={() => {
if (index > 0) {
setIndex(index - 1);
}
}}
>
<ChevronLeft
className="stroke-zinc-600 dark:stroke-zinc-50"
size={16}
/>
</button>
<button
type="button"
className={cn(
"pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950",
alwaysShow
? "opacity-100"
: "opacity-0 group-hover/hover:opacity-100",
alwaysShow
? "disabled:opacity-40"
: "disabled:group-hover/hover:opacity-40",
classNameButton
)}
disabled={index + 1 === itemsCount}
onClick={() => {
if (index < itemsCount - 1) {
setIndex(index + 1);
}
}}
>
<ChevronRight
className="stroke-zinc-600 dark:stroke-zinc-50"
size={16}
/>
</button>
</div>
);
}
type CarouselIndicatorProps = {
className?: string;
classNameButton?: string;
};
function CarouselIndicator({
className,
classNameButton,
}: CarouselIndicatorProps) {
const { index, itemsCount, setIndex } = useCarousel();
return (
<div
className={cn(
"absolute bottom-0 z-10 flex w-full items-center justify-center",
className
)}
>
<div className="flex space-x-2">
{Array.from({ length: itemsCount }, (_, i) => (
<button
key={i}
type="button"
aria-label={`Go to slide ${i + 1}`}
onClick={() => setIndex(i)}
className={cn(
"h-2 w-2 rounded-full transition-opacity duration-300",
index === i
? "bg-zinc-950 dark:bg-zinc-50"
: "bg-zinc-900/50 dark:bg-zinc-100/50",
classNameButton
)}
/>
))}
</div>
</div>
);
}
type CarouselContentProps = {
children: ReactNode;
className?: string;
transition?: Transition;
};
function CarouselContent({
children,
className,
transition,
}: CarouselContentProps) {
const { index, setIndex, setItemsCount, disableDrag } = useCarousel();
const [visibleItemsCount, setVisibleItemsCount] = useState(1);
const dragX = useMotionValue(0);
const containerRef = useRef<HTMLDivElement>(null);
const itemsLength = Children.count(children);
useEffect(() => {
if (!containerRef.current) {
return;
}
const options = {
root: containerRef.current,
threshold: 0.5,
};
const observer = new IntersectionObserver((entries) => {
const visibleCount = entries.filter(
(entry) => entry.isIntersecting
).length;
setVisibleItemsCount(visibleCount);
}, options);
const childNodes = containerRef.current.children;
Array.from(childNodes).forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, [children, setItemsCount]);
useEffect(() => {
if (!itemsLength) {
return;
}
setItemsCount(itemsLength);
}, [itemsLength, setItemsCount]);
const onDragEnd = () => {
const x = dragX.get();
if (x <= -10 && index < itemsLength - 1) {
setIndex(index + 1);
} else if (x >= 10 && index > 0) {
setIndex(index - 1);
}
};
return (
<motion.div
drag={disableDrag ? false : "x"}
dragConstraints={
disableDrag
? undefined
: {
left: 0,
right: 0,
}
}
dragMomentum={disableDrag ? undefined : false}
style={{
x: disableDrag ? undefined : dragX,
}}
animate={{
translateX: `-${index * (100 / visibleItemsCount)}%`,
}}
onDragEnd={disableDrag ? undefined : onDragEnd}
transition={
transition || {
damping: 18,
stiffness: 90,
type: "spring",
duration: 0.2,
}
}
className={cn(
"flex items-center",
!disableDrag && "cursor-grab active:cursor-grabbing",
className
)}
ref={containerRef}
>
{children}
</motion.div>
);
}
type CarouselItemProps = {
children: ReactNode;
className?: string;
};
function CarouselItem({ children, className }: CarouselItemProps) {
return (
<motion.div
className={cn(
"w-full min-w-0 shrink-0 grow-0 overflow-hidden",
className
)}
>
{children}
</motion.div>
);
}
export {
Carousel,
CarouselContent,
CarouselNavigation,
CarouselIndicator,
CarouselItem,
useCarousel,
};