"use client"; import * as React from "react"; import { useState, useCallback, useMemo, createContext, useContext, useId, } from "react"; import { motion, AnimatePresence, useReducedMotion } from "motion/react"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { cn } from "@/lib/utils"; export type AccordionType = "single" | "multiple"; export type AccordionVariant = "separated" | "bordered" | "ghost"; export interface AccordionItemData { value: string; title: React.ReactNode; subtitle?: React.ReactNode; badge?: React.ReactNode; icon?: React.ReactNode; content: React.ReactNode; disabled?: boolean; } interface AccordionContextValue { type: AccordionType; expanded: string[]; toggleItem: (val: string) => void; variant: AccordionVariant; blurAmount: number; } const AccordionContext = createContext(null); function useAccordion() { const context = useContext(AccordionContext); if (!context) { throw new Error("Accordion components must be used within an Accordion"); } return context; } interface AccordionItemContextValue { value: string; isOpen: boolean; disabled?: boolean; triggerId: string; contentId: string; } const AccordionItemContext = createContext( null, ); function useAccordionItem() { const context = useContext(AccordionItemContext); if (!context) { throw new Error( "AccordionTrigger and AccordionContent must be used within an AccordionItem", ); } return context; } export interface AccordionProps extends Omit< React.HTMLAttributes, "defaultValue" | "onChange" > { type?: AccordionType; value?: string | string[]; defaultValue?: string | string[]; onValueChange?: (value: string | string[]) => void; collapsible?: boolean; variant?: AccordionVariant; blurAmount?: number; items?: AccordionItemData[]; } export function Accordion({ type = "single", value: controlledValue, defaultValue, onValueChange, collapsible = true, variant = "separated", blurAmount = 8, items, children, className, ...props }: AccordionProps) { const [internalExpanded, setInternalExpanded] = useState(() => { if (defaultValue !== undefined) { return Array.isArray(defaultValue) ? defaultValue : [defaultValue]; } return []; }); const isControlled = controlledValue !== undefined; const currentExpanded = useMemo(() => { if (isControlled) { return Array.isArray(controlledValue) ? controlledValue : controlledValue ? [controlledValue] : []; } return internalExpanded; }, [isControlled, controlledValue, internalExpanded]); const toggleItem = useCallback( (itemValue: string) => { let next: string[]; if (type === "single") { const isCurrentOpen = currentExpanded.includes(itemValue); if (isCurrentOpen) { next = collapsible ? [] : [itemValue]; } else { next = [itemValue]; } } else { if (currentExpanded.includes(itemValue)) { next = currentExpanded.filter((v) => v !== itemValue); } else { next = [...currentExpanded, itemValue]; } } if (!isControlled) { setInternalExpanded(next); } if (onValueChange) { onValueChange(type === "single" ? (next[0] ?? "") : next); } }, [type, currentExpanded, collapsible, isControlled, onValueChange], ); return (
{items ? items.map((item) => ( {item.title} {item.content} )) : children}
); } export interface AccordionItemProps extends React.HTMLAttributes { value: string; disabled?: boolean; } export function AccordionItem({ value, disabled = false, className, children, ...props }: AccordionItemProps) { const { expanded, variant } = useAccordion(); const id = useId(); const triggerId = `accordion-trigger-${id}`; const contentId = `accordion-content-${id}`; const isOpen = expanded.includes(value); return (
{children}
); } export interface AccordionTriggerProps extends React.ButtonHTMLAttributes { badge?: React.ReactNode; icon?: React.ReactNode; subtitle?: React.ReactNode; } export function AccordionTrigger({ children, badge, icon, subtitle, className, ...props }: AccordionTriggerProps) { const { toggleItem } = useAccordion(); const { value, isOpen, disabled, triggerId, contentId } = useAccordionItem(); const prefersReducedMotion = useReducedMotion(); return ( ); } export type AccordionContentProps = React.HTMLAttributes; export function AccordionContent({ children, className, ...props }: AccordionContentProps) { const { blurAmount } = useAccordion(); const { isOpen, triggerId, contentId } = useAccordionItem(); const prefersReducedMotion = useReducedMotion(); return ( {isOpen && (
{children}
)}
); } export const SmoothAccordion = Accordion;