'use client'; import * as React from 'react'; import { AnimatePresence, motion, MotionConfig, Transition, Variant, Variants, } from 'motion/react'; import { createContext, useContext, useState, useId, useEffect } from 'react'; import { cn } from '@/lib/utils'; export type DisclosureContextType = { open: boolean; toggle: () => void; variants?: { expanded: Variant; collapsed: Variant }; }; const DisclosureContext = createContext<DisclosureContextType | undefined>( undefined ); export type DisclosureProviderProps = { children: React.ReactNode; open: boolean; onOpenChange?: (open: boolean) => void; variants?: { expanded: Variant; collapsed: Variant }; }; function DisclosureProvider({ children, open: openProp, onOpenChange, variants, }: DisclosureProviderProps) { const [internalOpenValue, setInternalOpenValue] = useState<boolean>(openProp); useEffect(() => { setInternalOpenValue(openProp); }, [openProp]); const toggle = () => { const newOpen = !internalOpenValue; setInternalOpenValue(newOpen); if (onOpenChange) { onOpenChange(newOpen); } }; return ( <DisclosureContext.Provider value={{ open: internalOpenValue, toggle, variants, }} > {children} </DisclosureContext.Provider> ); } function useDisclosure() { const context = useContext(DisclosureContext); if (!context) { throw new Error('useDisclosure must be used within a DisclosureProvider'); } return context; } export type DisclosureProps = { open?: boolean; onOpenChange?: (open: boolean) => void; children: React.ReactNode; className?: string; variants?: { expanded: Variant; collapsed: Variant }; transition?: Transition; }; export function Disclosure({ open: openProp = false, onOpenChange, children, className, transition, variants, }: DisclosureProps) { return ( <MotionConfig transition={transition}> <div className={className}> <DisclosureProvider open={openProp} onOpenChange={onOpenChange} variants={variants} > {React.Children.toArray(children)[0]} {React.Children.toArray(children)[1]} </DisclosureProvider> </div> </MotionConfig> ); } export function DisclosureTrigger({ children, className, }: { children: React.ReactNode; className?: string; }) { const { toggle, open } = useDisclosure(); return ( <> {React.Children.map(children, (child) => { return React.isValidElement(child) ? React.cloneElement(child, { onClick: toggle, role: 'button', 'aria-expanded': open, tabIndex: 0, onKeyDown: (e: { key: string; preventDefault: () => void }) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } }, className: cn( className, (child as React.ReactElement).props.className ), ...(child as React.ReactElement).props, }) : child; })} </> ); } export function DisclosureContent({ children, className, }: { children: React.ReactNode; className?: string; }) { const { open, variants } = useDisclosure(); const uniqueId = useId(); 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 ( <div className={cn('overflow-hidden', className)}> <AnimatePresence initial={false}> {open && ( <motion.div id={uniqueId} initial='collapsed' animate='expanded' exit='collapsed' variants={combinedVariants} > {children} </motion.div> )} </AnimatePresence> </div> ); } export default { Disclosure, DisclosureProvider, DisclosureTrigger, DisclosureContent, };