ComponentsAboutContactPrivacyTerms

Built by Lacy Morrow

    Close file
    Close file
    Close file
    Close file
    'use client';
    import {
      motion,
      AnimatePresence,
      Transition,
      Variants,
      Variant,
      MotionConfig,
    } from 'motion/react';
    import { cn } from '@/lib/utils';
    import React, { createContext, useContext, useState, ReactNode } from 'react';
    
    export type AccordionContextType = {
      expandedValue: React.Key | null;
      toggleItem: (value: React.Key) => void;
      variants?: { expanded: Variant; collapsed: Variant };
    };
    
    const AccordionContext = createContext<AccordionContextType | undefined>(
      undefined
    );
    
    function useAccordion() {
      const context = useContext(AccordionContext);
      if (!context) {
        throw new Error('useAccordion must be used within an AccordionProvider');
      }
      return context;
    }
    
    export type AccordionProviderProps = {
      children: ReactNode;
      variants?: { expanded: Variant; collapsed: Variant };
      expandedValue?: React.Key | null;
      onValueChange?: (value: React.Key | null) => void;
    };
    
    function AccordionProvider({
      children,
      variants,
      expandedValue: externalExpandedValue,
      onValueChange,
    }: AccordionProviderProps) {
      const [internalExpandedValue, setInternalExpandedValue] =
        useState<React.Key | null>(null);
    
      const expandedValue =
        externalExpandedValue !== undefined
          ? externalExpandedValue
          : internalExpandedValue;
    
      const toggleItem = (value: React.Key) => {
        const newValue = expandedValue === value ? null : value;
        if (onValueChange) {
          onValueChange(newValue);
        } else {
          setInternalExpandedValue(newValue);
        }
      };
    
      return (
        <AccordionContext.Provider value={{ expandedValue, toggleItem, variants }}>
          {children}
        </AccordionContext.Provider>
      );
    }
    
    export type AccordionProps = {
      children: ReactNode;
      className?: string;
      transition?: Transition;
      variants?: { expanded: Variant; collapsed: Variant };
      expandedValue?: React.Key | null;
      onValueChange?: (value: React.Key | null) => void;
    };
    
    function Accordion({
      children,
      className,
      transition,
      variants,
      expandedValue,
      onValueChange,
    }: AccordionProps) {
      return (
        <MotionConfig transition={transition}>
          <div className={cn('relative', className)} aria-orientation='vertical'>
            <AccordionProvider
              variants={variants}
              expandedValue={expandedValue}
              onValueChange={onValueChange}
            >
              {children}
            </AccordionProvider>
          </div>
        </MotionConfig>
      );
    }
    
    export type AccordionItemProps = {
      value: React.Key;
      children: ReactNode;
      className?: string;
    };
    
    function AccordionItem({ value, children, className }: AccordionItemProps) {
      const { expandedValue } = useAccordion();
      const isExpanded = value === expandedValue;
    
      return (
        <div
          className={cn('overflow-hidden', className)}
          {...(isExpanded ? { 'data-expanded': '' } : {'data-closed': ''})}
        >
          {React.Children.map(children, (child) => {
            if (React.isValidElement(child)) {
              return React.cloneElement(child, {
                ...child.props,
                value,
                expanded: isExpanded,
              });
            }
            return child;
          })}
        </div>
      );
    }
    
    export type AccordionTriggerProps = {
      children: ReactNode;
      className?: string;
    };
    
    function AccordionTrigger({
      children,
      className,
      ...props
    }: AccordionTriggerProps) {
      const { toggleItem, expandedValue } = useAccordion();
      const value = (props as { value?: React.Key }).value;
      const isExpanded = value === expandedValue;
    
      return (
        <button
          onClick={() => value !== undefined && toggleItem(value)}
          aria-expanded={isExpanded}
          type='button'
          className={cn('group', className)}
          {...(isExpanded ? { 'data-expanded': '' } : {'data-closed': ''})}
        >
          {children}
        </button>
      );
    }
    
    export type AccordionContentProps = {
      children: ReactNode;
      className?: string;
    };
    
    function AccordionContent({
      children,
      className,
      ...props
    }: AccordionContentProps) {
      const { expandedValue, variants } = useAccordion();
      const value = (props as { value?: React.Key }).value;
      const isExpanded = value === expandedValue;
    
      const BASE_VARIANTS: Variants = {
        expanded: { height: 'auto', opacity: 1 },
        collapsed: { height: 0, opacity: 0 },
      };
    
      const combinedVariants = {
        expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
        collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
      };
    
      return (
        <AnimatePresence initial={false}>
          {isExpanded && (
            <motion.div
              initial='collapsed'
              animate='expanded'
              exit='collapsed'
              variants={combinedVariants}
              className={className}
            >
              {children}
            </motion.div>
          )}
        </AnimatePresence>
      );
    }
    
    export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };