118 lines
2.8 KiB
TypeScript
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,
|
|
};
|