131 lines
3.6 KiB
TypeScript
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,
|
|
};
|