'use client';
import {
motion,
AnimatePresence,
Transition,
Variants,
Variant,
MotionConfig,
} from 'framer-motion';
import { cn } from '@/lib/utils';
import React, { createContext, useContext, useState, ReactNode } from 'react';
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;
}
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>
);
}
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>
);
}
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': '' } : {})}
>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
...child.props,
value,
expanded: isExpanded,
});
}
return child;
})}
</div>
);
}
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': '' } : {})}
>
{children}
</button>
);
}
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 };