2025-08-20 04:12:49 -06:00

118 lines
2.8 KiB
TypeScript

'use client';
import * as React from 'react';
import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
import {
AnimatePresence,
motion,
type HTMLMotionProps,
type Transition,
} from 'motion/react';
type CollapsibleContextType = {
isOpen: boolean;
};
const CollapsibleContext = React.createContext<
CollapsibleContextType | undefined
>(undefined);
const useCollapsible = (): CollapsibleContextType => {
const context = React.useContext(CollapsibleContext);
if (!context) {
throw new Error('useCollapsible must be used within a Collapsible');
}
return context;
};
type CollapsibleProps = React.ComponentProps<typeof CollapsiblePrimitive.Root>;
function Collapsible({ children, ...props }: CollapsibleProps) {
const [isOpen, setIsOpen] = React.useState(
props?.open ?? props?.defaultOpen ?? false,
);
React.useEffect(() => {
if (props?.open !== undefined) setIsOpen(props.open);
}, [props?.open]);
const handleOpenChange = React.useCallback(
(open: boolean) => {
setIsOpen(open);
props.onOpenChange?.(open);
},
[props],
);
return (
<CollapsibleContext.Provider value={{ isOpen }}>
<CollapsiblePrimitive.Root
data-slot="collapsible"
{...props}
onOpenChange={handleOpenChange}
>
{children}
</CollapsiblePrimitive.Root>
</CollapsibleContext.Provider>
);
}
type CollapsibleTriggerProps = React.ComponentProps<
typeof CollapsiblePrimitive.Trigger
>;
function CollapsibleTrigger(props: CollapsibleTriggerProps) {
return (
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
);
}
type CollapsibleContentProps = React.ComponentProps<
typeof CollapsiblePrimitive.Content
> &
HTMLMotionProps<'div'> & {
transition?: Transition;
};
function CollapsibleContent({
className,
children,
transition = { type: 'spring', stiffness: 150, damping: 22 },
...props
}: CollapsibleContentProps) {
const { isOpen } = useCollapsible();
return (
<AnimatePresence>
{isOpen && (
<CollapsiblePrimitive.Content asChild forceMount {...props}>
<motion.div
key="collapsible-content"
data-slot="collapsible-content"
layout
initial={{ opacity: 0, height: 0, overflow: 'hidden' }}
animate={{ opacity: 1, height: 'auto', overflow: 'hidden' }}
exit={{ opacity: 0, height: 0, overflow: 'hidden' }}
transition={transition}
className={className}
{...props}
>
{children}
</motion.div>
</CollapsiblePrimitive.Content>
)}
</AnimatePresence>
);
}
export {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
useCollapsible,
type CollapsibleContextType,
type CollapsibleProps,
type CollapsibleTriggerProps,
type CollapsibleContentProps,
};