ComponentsAboutContactPrivacyTerms

Built by Lacy Morrow

    Close file
    Close file
    Close file
    Close file
    Close file
    'use client';
    import {
      Children,
      ReactNode,
      createContext,
      useContext,
      useEffect,
      useRef,
      useState,
    } from 'react';
    import { motion, Transition, useMotionValue } from 'motion/react';
    import { cn } from '@/lib/utils';
    import { ChevronLeft, ChevronRight } from 'lucide-react';
    
    export 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;
    }
    
    export 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>
      );
    }
    
    export 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>
      );
    }
    
    export 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'
            aria-label='Previous slide'
            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'
                : 'group-hover/hover:disabled: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'
                : 'group-hover/hover:disabled:opacity-40',
              classNameButton
            )}
            aria-label='Next slide'
            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>
      );
    }
    
    export 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>
      );
    }
    
    export 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>
      );
    }
    
    export 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,
    };