227 lines
5.6 KiB
TypeScript
227 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { motion, type Transition } from 'motion/react';
|
|
import {
|
|
TabGroup as TabGroupPrimitive,
|
|
TabList as TabListPrimitive,
|
|
Tab as TabPrimitive,
|
|
TabPanels as TabPanelsPrimitive,
|
|
TabPanel as TabPanelPrimitive,
|
|
type TabGroupProps as TabGroupPrimitiveProps,
|
|
type TabListProps as TabListPrimitiveProps,
|
|
type TabProps as TabPrimitiveProps,
|
|
type TabPanelsProps as TabPanelsPrimitiveProps,
|
|
type TabPanelProps as TabPanelPrimitiveProps,
|
|
} from '@headlessui/react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import {
|
|
MotionHighlight,
|
|
MotionHighlightItem,
|
|
} from '@/registry/effects/motion-highlight';
|
|
|
|
type TabGroupProps<TTag extends React.ElementType = 'div'> =
|
|
TabGroupPrimitiveProps<TTag> & {
|
|
className?: string;
|
|
as?: TTag;
|
|
};
|
|
|
|
function TabGroup<TTag extends React.ElementType = 'div'>({
|
|
className,
|
|
...props
|
|
}: TabGroupProps<TTag>) {
|
|
return (
|
|
<TabGroupPrimitive
|
|
data-slot="tab-group"
|
|
className={cn('flex flex-col gap-2', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type TabListProps<TTag extends React.ElementType = 'div'> =
|
|
TabListPrimitiveProps<TTag> & {
|
|
as?: TTag;
|
|
className?: string;
|
|
activeClassName?: string;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabList<TTag extends React.ElementType = 'div'>({
|
|
children,
|
|
className,
|
|
activeClassName,
|
|
transition = {
|
|
type: 'spring',
|
|
stiffness: 200,
|
|
damping: 25,
|
|
},
|
|
...props
|
|
}: TabListProps<TTag>) {
|
|
return (
|
|
<TabListPrimitive
|
|
data-slot="tab-list"
|
|
className={cn(
|
|
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{(bag) => (
|
|
<MotionHighlight
|
|
controlledItems
|
|
className={cn('rounded-sm bg-background shadow-sm', activeClassName)}
|
|
value={bag.selectedIndex.toString()}
|
|
transition={transition}
|
|
>
|
|
{typeof children === 'function' ? children(bag) : children}
|
|
</MotionHighlight>
|
|
)}
|
|
</TabListPrimitive>
|
|
);
|
|
}
|
|
|
|
type TabProps<TTag extends React.ElementType = 'button'> = Omit<
|
|
TabPrimitiveProps<TTag>,
|
|
'children'
|
|
> &
|
|
Required<Pick<TabPrimitiveProps<TTag>, 'children'>> & {
|
|
index: number;
|
|
className?: string;
|
|
as?: TTag;
|
|
};
|
|
|
|
function Tab<TTag extends React.ElementType = 'button'>(props: TabProps<TTag>) {
|
|
const { children, className, index, as = 'button', ...rest } = props;
|
|
|
|
return (
|
|
<MotionHighlightItem value={index.toString()} className="size-full">
|
|
<TabPrimitive
|
|
data-slot="tabs-trigger"
|
|
className={cn(
|
|
'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-selected:text-foreground z-[1]',
|
|
className,
|
|
)}
|
|
as={as as React.ElementType}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</TabPrimitive>
|
|
</MotionHighlightItem>
|
|
);
|
|
}
|
|
|
|
type TabPanelProps<TTag extends React.ElementType = typeof motion.div> = Omit<
|
|
TabPanelPrimitiveProps<TTag>,
|
|
'transition'
|
|
> & {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
as?: TTag;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabPanel<TTag extends React.ElementType = typeof motion.div>(
|
|
props: TabPanelProps<TTag>,
|
|
) {
|
|
const {
|
|
className,
|
|
as = motion.div,
|
|
transition = {
|
|
duration: 0.5,
|
|
ease: 'easeInOut',
|
|
},
|
|
...rest
|
|
} = props;
|
|
|
|
return (
|
|
<TabPanelPrimitive
|
|
data-slot="tabs-content"
|
|
className={cn('flex-1 outline-none', className)}
|
|
layout
|
|
initial={{ opacity: 0, y: -10, filter: 'blur(4px)' }}
|
|
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
|
exit={{ opacity: 0, y: 10, filter: 'blur(4px)' }}
|
|
transition={transition}
|
|
as={as as React.ElementType}
|
|
{...rest}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type TabPanelsProps<TTag extends React.ElementType = typeof motion.div> = Omit<
|
|
TabPanelsPrimitiveProps<TTag>,
|
|
'transition'
|
|
> & {
|
|
className?: string;
|
|
as?: TTag;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabPanels<TTag extends React.ElementType = typeof motion.div>(
|
|
props: TabPanelsProps<TTag>,
|
|
) {
|
|
const {
|
|
children,
|
|
className,
|
|
as = motion.div,
|
|
transition = { type: 'spring', stiffness: 200, damping: 25 },
|
|
...rest
|
|
} = props;
|
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
|
|
|
const [height, setHeight] = React.useState(0);
|
|
|
|
React.useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
const newHeight = entries[0]?.contentRect.height ?? 0;
|
|
requestAnimationFrame(() => {
|
|
setHeight(newHeight);
|
|
});
|
|
});
|
|
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
};
|
|
}, [children]);
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (containerRef.current) {
|
|
const initialHeight = containerRef.current.getBoundingClientRect().height;
|
|
setHeight(initialHeight);
|
|
}
|
|
}, [children]);
|
|
|
|
return (
|
|
<TabPanelsPrimitive
|
|
data-slot="tabs-contents"
|
|
layout
|
|
animate={{ height: height }}
|
|
transition={transition}
|
|
as={as as React.ElementType}
|
|
className={className}
|
|
{...rest}
|
|
>
|
|
<div ref={containerRef}>{children}</div>
|
|
</TabPanelsPrimitive>
|
|
);
|
|
}
|
|
|
|
export {
|
|
TabGroup,
|
|
TabList,
|
|
Tab,
|
|
TabPanel,
|
|
TabPanels,
|
|
type TabGroupProps,
|
|
type TabListProps,
|
|
type TabProps,
|
|
type TabPanelProps,
|
|
type TabPanelsProps,
|
|
};
|