import { createContext, forwardRef, useCallback, useContext, useId, useMemo, useState, type HTMLAttributes, type ReactNode, } from 'react' import { cn } from '@/lib/utils' const ChevronIcon = () => ( ) // --- Context --- interface AccordionContextValue { openItems: Set toggle: (value: string) => void } const AccordionContext = createContext(null) function useAccordionContext() { const ctx = useContext(AccordionContext) if (!ctx) throw new Error('Accordion components must be used within an Accordion') return ctx } interface AccordionItemContextValue { value: string isOpen: boolean triggerId: string contentId: string disabled: boolean } const AccordionItemContext = createContext(null) function useAccordionItemContext() { const ctx = useContext(AccordionItemContext) if (!ctx) throw new Error('AccordionItem sub-components must be used within an AccordionItem') return ctx } // --- Accordion --- export interface AccordionProps extends HTMLAttributes { type?: 'single' | 'multiple' collapsible?: boolean defaultValue?: string | string[] value?: string | string[] onValueChange?: (value: string | string[]) => void } export const Accordion = forwardRef( ( { type = 'single', collapsible = false, defaultValue, value, onValueChange, className, children, ...props }, ref, ) => { const [internalOpen, setInternalOpen] = useState>(() => { if (defaultValue) return new Set(Array.isArray(defaultValue) ? defaultValue : [defaultValue]) return new Set() }) const isControlled = value !== undefined const openItems = isControlled ? new Set(Array.isArray(value) ? value : value ? [value] : []) : internalOpen const toggle = useCallback( (itemValue: string) => { const compute = (prev: Set): Set => { const next = new Set(prev) if (next.has(itemValue)) { if (type === 'single' && !collapsible) return prev next.delete(itemValue) } else { if (type === 'single') next.clear() next.add(itemValue) } return next } if (isControlled) { const next = compute(openItems) if (next !== openItems) { onValueChange?.(type === 'single' ? ([...next][0] ?? '') : [...next]) } } else { setInternalOpen((prev) => { const next = compute(prev) if (next !== prev) { onValueChange?.(type === 'single' ? ([...next][0] ?? '') : [...next]) } return next }) } }, [type, collapsible, isControlled, openItems, onValueChange], ) const contextValue = useMemo(() => ({ openItems, toggle }), [openItems, toggle]) return (
{children}
) }, ) Accordion.displayName = 'Accordion' // --- AccordionItem --- export interface AccordionItemProps extends HTMLAttributes { value: string disabled?: boolean } export const AccordionItem = forwardRef( ({ value, disabled = false, className, children, ...props }, ref) => { const { openItems } = useAccordionContext() const isOpen = openItems.has(value) const id = useId() const triggerId = `${id}-trigger` const contentId = `${id}-content` const itemContext = useMemo( () => ({ value, isOpen, triggerId, contentId, disabled }), [value, isOpen, triggerId, contentId, disabled], ) return (
{children}
) }, ) AccordionItem.displayName = 'AccordionItem' // --- AccordionTrigger --- export interface AccordionTriggerProps extends Omit, 'children'> { icon?: ReactNode children: ReactNode } export const AccordionTrigger = forwardRef( ({ icon, className, children, ...props }, ref) => { const { toggle } = useAccordionContext() const { value, isOpen, triggerId, contentId, disabled } = useAccordionItemContext() return ( ) }, ) AccordionTrigger.displayName = 'AccordionTrigger' // --- AccordionContent --- export interface AccordionContentProps extends HTMLAttributes {} export const AccordionContent = forwardRef( ({ className, children, ...props }, ref) => { const { isOpen, triggerId, contentId } = useAccordionItemContext() return (
{children}
) }, ) AccordionContent.displayName = 'AccordionContent'