'use client'
import * as React from 'react'
import { ChevronDown } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { cn } from '@/lib/utils'
interface CollapsibleContextValue {
isOpen: boolean
toggle: () => void
}
const CollapsibleContext = React.createContext<CollapsibleContextValue | null>(null)
function useCollapsible() {
const context = React.useContext(CollapsibleContext)
if (!context) {
throw new Error('useCollapsible must be used within a Collapsible')
}
return context
}
interface CollapsibleProps {
children: React.ReactNode
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
className?: string
}
function Collapsible({
children,
defaultOpen = false,
open: controlledOpen,
onOpenChange,
className,
}: CollapsibleProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const isOpen = controlledOpen ?? uncontrolledOpen
const toggle = React.useCallback(() => {
const newValue = !isOpen
setUncontrolledOpen(newValue)
onOpenChange?.(newValue)
}, [isOpen, onOpenChange])
return (
<CollapsibleContext.Provider value={{ isOpen, toggle }}>
<div className={className}>{children}</div>
</CollapsibleContext.Provider>
)
}
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean
showIcon?: boolean
}
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
({ className, children, showIcon = true, ...props }, ref) => {
const { isOpen, toggle } = useCollapsible()
return (
<button
ref={ref}
type="button"
onClick={toggle}
className={cn(
'flex w-full items-center justify-between transition-colors',
className
)}
{...props}
>
{children}
{showIcon && (
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
className="ml-2"
>
<ChevronDown className="h-4 w-4 text-[hsl(var(--muted-foreground))]" />
</motion.span>
)}
</button>
)
}
)
CollapsibleTrigger.displayName = 'CollapsibleTrigger'
interface CollapsibleContentProps extends React.HTMLAttributes<HTMLDivElement> {}
function CollapsibleContent({ className, children, ...props }: CollapsibleContentProps) {
const { isOpen } = useCollapsible()
return (
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: [0, 0, 0.2, 1] }}
className="overflow-hidden"
>
<div className={cn('pt-2', className)} {...props}>
{children}
</div>
</motion.div>
)}
</AnimatePresence>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent, useCollapsible }