import { motion, AnimatePresence } from 'framer-motion';
import Autoplay from 'embla-carousel-autoplay';
import useEmblaCarousel from 'embla-carousel-react';
import ClassNames from 'embla-carousel-class-names';
import { cn } from '@/lib/utils';
type UseDotButtonType = {
onDotButtonClick: (index: number) => void;
interface CarouselProps {
children: React.ReactNode;
options: EmblaOptionsType;
interface ThumbnailSlide {
interface CarouselContextType {
prevBtnDisabled: boolean;
nextBtnDisabled: boolean;
onPrevButtonClick: () => void;
onNextButtonClick: () => void;
slidesrArr: ThumbnailSlide[];
const CarouselContext = createContext<CarouselContextType | undefined>(
const TWEEN_FACTOR_BASE = 0.52;
const numberWithinRange = (number: number, min: number, max: number): number =>
Math.min(Math.max(number, min), max);
export const useCarouselContext = () => {
const context = useContext(CarouselContext);
'useCarouselContext must be used within a CarouselProvider'
const Carousel: React.FC<CarouselProps> = ({
const carouselId = useId();
const [slidesrArr, setSlidesArr] = useState<Element[]>([]);
plugins.push(ClassNames());
stopOnInteraction: false,
const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins);
const [selectedThumbIndex, setSelectedThumbIndex] = useState(0);
const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel({
containScroll: 'keepSnaps',
const onThumbClick = useCallback(
if (!emblaApi || !emblaThumbsApi) return;
emblaApi.scrollTo(index);
[emblaApi, emblaThumbsApi]
const onSelect = useCallback(() => {
if (!emblaApi || !emblaThumbsApi) return;
setSelectedThumbIndex(emblaApi.selectedScrollSnap()); // Use setSelectedThumbIndex here
emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap());
}, [emblaApi, emblaThumbsApi, setSelectedThumbIndex]);
emblaApi.on('select', onSelect);
emblaApi.on('reInit', onSelect);
}, [emblaApi, onSelect]);
const { selectedIndex, scrollSnaps, onDotButtonClick } =
const [scrollProgress, setScrollProgress] = useState(0);
} = usePrevNextButtons(emblaApi);
const onScroll = useCallback((emblaApi: EmblaCarouselType) => {
const progress = Math.max(0, Math.min(1, emblaApi.scrollProgress()));
setScrollProgress(progress * 100);
emblaApi.on('reInit', onScroll);
emblaApi.on('scroll', onScroll);
}, [emblaApi, onScroll]);
const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi);
const tweenFactor = useRef(0);
const tweenNodes = useRef<HTMLElement[]>([]);
const setTweenNodes = useCallback(
(emblaApi: EmblaCarouselType): void => {
tweenNodes.current = emblaApi.slideNodes().map((slideNode, index) => {
const node = slideNode.querySelector('.slider_content') as HTMLElement;
console.warn(`No .slider_content found for slide ${index}`);
const setTweenFactor = useCallback(
(emblaApi: EmblaCarouselType) => {
TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length;
const tweenScale = useCallback(
(emblaApi: EmblaCarouselType, eventName?: EmblaEventType) => {
const engine = emblaApi.internalEngine();
const scrollProgress = emblaApi.scrollProgress();
const slidesInView = emblaApi.slidesInView();
const isScrollEvent = eventName === 'scroll';
emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {
let diffToTarget = scrollSnap - scrollProgress;
const slidesInSnap = engine.slideRegistry[snapIndex];
slidesInSnap.forEach((slideIndex) => {
if (isScrollEvent && !slidesInView.includes(slideIndex)) return;
if (engine.options.loop) {
engine.slideLooper.loopPoints.forEach((loopItem) => {
const target = loopItem.target();
if (slideIndex === loopItem.index && target !== 0) {
const sign = Math.sign(target);
diffToTarget = scrollSnap - (1 + scrollProgress);
diffToTarget = scrollSnap + (1 - scrollProgress);
const tweenValue = 1 - Math.abs(diffToTarget * tweenFactor.current);
const scale = numberWithinRange(tweenValue, 0, 1).toString();
const tweenNode = tweenNodes.current[slideIndex];
tweenNode.style.transform = `scale(${scale})`;
setTweenFactor(emblaApi);
.on('reInit', setTweenNodes)
.on('reInit', setTweenFactor)
.on('reInit', tweenScale)
.on('scroll', tweenScale);
}, [emblaApi, tweenScale, isScale, setTweenNodes, setTweenFactor]);
<CarouselContext.Provider
className={cn(className, 'overflow-hidden rounded-md ')}
</CarouselContext.Provider>
children: React.ReactNode;
export const SliderContainer = ({
className={cn('flex', className)}
style={{ touchAction: 'pan-y pinch-zoom' }}
export const Slider: React.FC<SliderProps> = ({
const { isScale, setSlidesArr } = useCarouselContext();
// console.log(thumnailSrc)
const addImgToSlider = useCallback(() => {
setSlidesArr((prev: any) => {
// Prevent adding duplicate images
return [...prev, thumnailSrc];
}, [setSlidesArr, thumnailSrc]);
<div className={cn('min-w-0 flex-grow-0 flex-shrink-0', className)}>
<div className='slider_content'>{children}</div>
export const SliderPrevButton = ({
const { onPrevButtonClick, prevBtnDisabled }: any = useCarouselContext();
className={cn('', className)}
onClick={onPrevButtonClick}
disabled={prevBtnDisabled}
export const SliderNextButton = ({
const { onNextButtonClick, nextBtnDisabled }: any = useCarouselContext();
className={cn('', className)}
onClick={onNextButtonClick}
disabled={nextBtnDisabled}
export const SliderProgress = ({ className }: { className?: string }) => {
const { scrollProgress }: any = useCarouselContext();
' bg-gray-500 relative rounded-md h-2 justify-end items-center w-96 max-w-[90%] overflow-hidden',
className='dark:bg-white bg-black absolute w-full top-0 -left-[100%] bottom-0'
style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }}
export const SliderSnapDisplay = ({ className }: { className?: string }) => {
const { selectedSnap, snapCount } = useCarouselContext();
const prevSnapRef = useRef(selectedSnap);
const [direction, setDirection] = useState<number>(0);
setDirection(selectedSnap > prevSnapRef.current ? 1 : -1);
prevSnapRef.current = selectedSnap;
'mix-blend-difference overflow-hidden flex gap-1 items-center',
initial={(d: number) => ({ y: d * 20, opacity: 0 })}
animate={{ y: 0, opacity: 1 }}
exit={(d: number) => ({ y: d * -20, opacity: 0 })}
<span>/ {snapCount}</span>
export const SliderDotButton = ({
const { selectedIndex, scrollSnaps, onDotButtonClick, carouselId }: any =
<div className={cn('flex', className)}>
<div className='flex gap-2'>
{scrollSnaps.map((_: any, index: React.Key | null | undefined) => (
onClick={() => onDotButtonClick(index)}
className={`relative inline-flex p-0 m-0 w-10 h-2 `}
<div className=' bg-gray-500/40 h-2 rounded-full w-10 '></div>
{index === selectedIndex && (
<AnimatePresence mode='wait'>
layoutId={`hover-${carouselId}`}
'absolute z-[3] w-full h-full left-0 top-0 dark:bg-white bg-black rounded-full',
export const useDotButton = (
emblaApi: EmblaCarouselType | undefined
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const onDotButtonClick = useCallback(
emblaApi.scrollTo(index);
const onInit = useCallback((emblaApi: EmblaCarouselType) => {
setScrollSnaps(emblaApi.scrollSnapList());
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap());
emblaApi.on('reInit', onInit);
emblaApi.on('reInit', onSelect);
emblaApi.on('select', onSelect);
}, [emblaApi, onInit, onSelect]);
type UsePrevNextButtonsType = {
prevBtnDisabled: boolean;
nextBtnDisabled: boolean;
onPrevButtonClick: () => void;
onNextButtonClick: () => void;
export const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined
): UsePrevNextButtonsType => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
const onPrevButtonClick = useCallback(() => {
const onNextButtonClick = useCallback(() => {
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canScrollPrev());
setNextBtnDisabled(!emblaApi.canScrollNext());
emblaApi.on('reInit', onSelect);
emblaApi.on('select', onSelect);
}, [emblaApi, onSelect]);
type UseSelectedSnapDisplayType = {
export const useSelectedSnapDisplay = (
emblaApi: EmblaCarouselType | undefined
): UseSelectedSnapDisplayType => {
const [selectedSnap, setSelectedSnap] = useState(0);
const [snapCount, setSnapCount] = useState(0);
const updateScrollSnapState = useCallback((emblaApi: EmblaCarouselType) => {
setSnapCount(emblaApi.scrollSnapList().length);
setSelectedSnap(emblaApi.selectedScrollSnap());
updateScrollSnapState(emblaApi);
emblaApi.on('select', updateScrollSnapState);
emblaApi.on('reInit', updateScrollSnapState);
}, [emblaApi, updateScrollSnapState]);
export const ThumsSlider: React.FC = () => {
const { emblaThumbsRef, slidesrArr, selectedIndex, onThumbClick } =
// console.log(slidesrArr);
<div className='overflow-hidden mt-2' ref={emblaThumbsRef}>
<div className='flex flex-row gap-2'>
{slidesrArr.map((slide, index) => (
className={`min-w-0 w-full xl:h-24 aspect-auto border-2 rounded-md ${
: 'border-transparent opacity-30'
style={{ flex: '0 0 15%' }}
onClick={() => onThumbClick(index)}
className='w-full h-full object-cover rounded-sm'
alt={slide.alt || `Thumbnail ${index + 1}`}