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

131 lines
3.6 KiB
TypeScript

'use client';
import * as React from 'react';
import {
Disclosure as DisclosurePrimitive,
DisclosureButton as DisclosureButtonPrimitive,
DisclosurePanel as DisclosurePanelPrimitive,
type DisclosureProps as DisclosurePrimitiveProps,
type DisclosureButtonProps as DisclosureButtonPrimitiveProps,
type DisclosurePanelProps as DisclosurePanelPrimitiveProps,
} from '@headlessui/react';
import {
AnimatePresence,
motion,
type HTMLMotionProps,
type Transition,
} from 'motion/react';
import { cn } from '@workspace/ui/lib/utils';
type DisclosureContextType = {
isOpen: boolean;
};
const DisclosureContext = React.createContext<
DisclosureContextType | undefined
>(undefined);
const useDisclosure = (): DisclosureContextType => {
const context = React.useContext(DisclosureContext);
if (!context) {
throw new Error('useDisclosure must be used within a Disclosure');
}
return context;
};
type DisclosureProps<TTag extends React.ElementType = 'div'> =
DisclosurePrimitiveProps<TTag> & {
as?: TTag;
};
function Disclosure<TTag extends React.ElementType = 'div'>({
children,
...props
}: DisclosureProps<TTag>) {
return (
<DisclosurePrimitive data-slot="disclosure" {...props}>
{(bag) => (
<DisclosureContext.Provider value={{ isOpen: bag.open }}>
{typeof children === 'function' ? children(bag) : children}
</DisclosureContext.Provider>
)}
</DisclosurePrimitive>
);
}
type DisclosureButtonProps<TTag extends React.ElementType = 'button'> =
DisclosureButtonPrimitiveProps<TTag> & {
as?: TTag;
};
function DisclosureButton<TTag extends React.ElementType = 'button'>(
props: DisclosureButtonProps<TTag>,
) {
return <DisclosureButtonPrimitive data-slot="disclosure-button" {...props} />;
}
type DisclosurePanelProps<TTag extends React.ElementType = typeof motion.div> =
Pick<DisclosurePanelPrimitiveProps<TTag>, 'static' | 'unmount' | 'children'> &
Omit<HTMLMotionProps<'div'>, 'children'> & {
transition?: Transition;
as?: TTag;
};
function DisclosurePanel<TTag extends React.ElementType = typeof motion.div>(
props: DisclosurePanelProps<TTag>,
) {
const {
className,
children,
transition = { type: 'spring', stiffness: 150, damping: 22 },
as = motion.div,
unmount,
...rest
} = props;
const { isOpen } = useDisclosure();
return (
<AnimatePresence>
{isOpen && (
<DisclosurePanelPrimitive
static
as={as as React.ElementType}
unmount={unmount}
>
{(bag) => (
<motion.div
key="disclosure-panel"
data-slot="disclosure-panel"
initial={{ height: 0, opacity: 0, '--mask-stop': '0%' }}
animate={{ height: 'auto', opacity: 1, '--mask-stop': '100%' }}
exit={{ height: 0, opacity: 0, '--mask-stop': '0%' }}
transition={transition}
style={{
maskImage:
'linear-gradient(black var(--mask-stop), transparent var(--mask-stop))',
WebkitMaskImage:
'linear-gradient(black var(--mask-stop), transparent var(--mask-stop))',
}}
className={cn('overflow-hidden', className)}
{...rest}
>
{typeof children === 'function' ? children(bag) : children}
</motion.div>
)}
</DisclosurePanelPrimitive>
)}
</AnimatePresence>
);
}
export {
Disclosure,
DisclosureButton,
DisclosurePanel,
useDisclosure,
type DisclosureProps,
type DisclosureButtonProps,
type DisclosurePanelProps,
};