"use client"; import * as React from "react"; import { ChevronDown } from "lucide-react"; import { cn } from "@/lib/utils"; type AccordionType = "single" | "multiple"; interface AccordionContextValue { type: AccordionType; value: string | string[]; onValueChange: (value: string) => void; collapsible?: boolean; } const AccordionContext = React.createContext( null ); interface AccordionProps extends React.HTMLAttributes { type?: "single" | "multiple"; value?: string | string[]; defaultValue?: string | string[]; onValueChange?: (value: string | string[]) => void; collapsible?: boolean; } const Accordion = React.forwardRef( ( { type = "single", value, defaultValue, onValueChange, collapsible = false, className, children, ...props }, ref ) => { const [internalValue, setInternalValue] = React.useState( () => { if (value !== undefined) return value; if (defaultValue !== undefined) return defaultValue; return type === "single" ? "" : []; } ); const currentValue = value !== undefined ? value : internalValue; const handleValueChange = React.useCallback( (itemValue: string) => { let newValue: string | string[]; if (type === "single") { if (currentValue === itemValue && collapsible) { newValue = ""; } else if (currentValue === itemValue && !collapsible) { return; } else { newValue = itemValue; } } else { const currentArray = Array.isArray(currentValue) ? currentValue : [currentValue].filter(Boolean); if (currentArray.includes(itemValue)) { newValue = currentArray.filter((v) => v !== itemValue); } else { newValue = [...currentArray, itemValue]; } } if (value === undefined) { setInternalValue(newValue); } onValueChange?.(newValue); }, [type, currentValue, collapsible, value, onValueChange] ); const contextValue = React.useMemo( () => ({ type, value: currentValue, onValueChange: handleValueChange, collapsible, }), [type, currentValue, handleValueChange, collapsible] ); return (
{children}
); } ); Accordion.displayName = "Accordion"; interface AccordionItemContextValue { value: string; isOpen: boolean; } const AccordionItemContext = React.createContext(null); interface AccordionItemProps extends React.HTMLAttributes { value: string; } const AccordionItem = React.forwardRef( ({ className, value, children, ...props }, ref) => { const accordionContext = React.useContext(AccordionContext); if (!accordionContext) { throw new Error("AccordionItem must be used within an Accordion"); } const isOpen = Array.isArray(accordionContext.value) ? accordionContext.value.includes(value) : accordionContext.value === value; const contextValue = React.useMemo( () => ({ value, isOpen }), [value, isOpen] ); return (
{children}
); } ); AccordionItem.displayName = "AccordionItem"; interface AccordionTriggerProps extends React.ButtonHTMLAttributes {} const AccordionTrigger = React.forwardRef< HTMLButtonElement, AccordionTriggerProps >(({ className, children, ...props }, ref) => { const accordionContext = React.useContext(AccordionContext); const itemContext = React.useContext(AccordionItemContext); if (!accordionContext || !itemContext) { throw new Error("AccordionTrigger must be used within an AccordionItem"); } const { onValueChange } = accordionContext; const { value, isOpen } = itemContext; return (
); }); AccordionTrigger.displayName = "AccordionTrigger"; interface AccordionContentProps extends React.HTMLAttributes {} const AccordionContent = React.forwardRef( ({ className, children, ...props }, ref) => { const itemContext = React.useContext(AccordionItemContext); const contentRef = React.useRef(null); const [height, setHeight] = React.useState(undefined); if (!itemContext) { throw new Error("AccordionContent must be used within an AccordionItem"); } const { isOpen } = itemContext; React.useEffect(() => { if (contentRef.current) { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { setHeight(entry.contentRect.height); } }); resizeObserver.observe(contentRef.current); return () => resizeObserver.disconnect(); } }, []); return (
{children}
); } ); AccordionContent.displayName = "AccordionContent"; export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };