215 lines
5.3 KiB
TypeScript
215 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Tabs as TabsPrimitive } from 'radix-ui';
|
|
import { type HTMLMotionProps, type Transition, motion } from 'motion/react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import {
|
|
MotionHighlight,
|
|
MotionHighlightItem,
|
|
} from '@/registry/effects/motion-highlight';
|
|
|
|
type TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>;
|
|
|
|
function Tabs({ className, ...props }: TabsProps) {
|
|
return (
|
|
<TabsPrimitive.Root
|
|
data-slot="tabs"
|
|
className={cn('flex flex-col gap-2', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List> & {
|
|
activeClassName?: string;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabsList({
|
|
ref,
|
|
children,
|
|
className,
|
|
activeClassName,
|
|
transition = {
|
|
type: 'spring',
|
|
stiffness: 200,
|
|
damping: 25,
|
|
},
|
|
...props
|
|
}: TabsListProps) {
|
|
const localRef = React.useRef<HTMLDivElement | null>(null);
|
|
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
|
|
|
|
const [activeValue, setActiveValue] = React.useState<string | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const getActiveValue = React.useCallback(() => {
|
|
if (!localRef.current) return;
|
|
const activeTab = localRef.current.querySelector<HTMLElement>(
|
|
'[data-state="active"]',
|
|
);
|
|
if (!activeTab) return;
|
|
setActiveValue(activeTab.getAttribute('data-value') ?? undefined);
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
getActiveValue();
|
|
|
|
const observer = new MutationObserver(getActiveValue);
|
|
|
|
if (localRef.current) {
|
|
observer.observe(localRef.current, {
|
|
attributes: true,
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, [getActiveValue]);
|
|
|
|
return (
|
|
<MotionHighlight
|
|
controlledItems
|
|
className={cn('rounded-sm bg-background shadow-sm', activeClassName)}
|
|
value={activeValue}
|
|
transition={transition}
|
|
>
|
|
<TabsPrimitive.List
|
|
ref={localRef}
|
|
data-slot="tabs-list"
|
|
className={cn(
|
|
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</TabsPrimitive.List>
|
|
</MotionHighlight>
|
|
);
|
|
}
|
|
|
|
type TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Trigger>;
|
|
|
|
function TabsTrigger({ className, value, ...props }: TabsTriggerProps) {
|
|
return (
|
|
<MotionHighlightItem value={value} className="size-full">
|
|
<TabsPrimitive.Trigger
|
|
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-[state=active]:text-foreground z-[1]',
|
|
className,
|
|
)}
|
|
value={value}
|
|
{...props}
|
|
/>
|
|
</MotionHighlightItem>
|
|
);
|
|
}
|
|
|
|
type TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Content> &
|
|
HTMLMotionProps<'div'> & {
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabsContent({
|
|
className,
|
|
children,
|
|
transition = {
|
|
duration: 0.5,
|
|
ease: 'easeInOut',
|
|
},
|
|
...props
|
|
}: TabsContentProps) {
|
|
return (
|
|
<TabsPrimitive.Content asChild {...props}>
|
|
<motion.div
|
|
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}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</TabsPrimitive.Content>
|
|
);
|
|
}
|
|
|
|
type TabsContentsProps = HTMLMotionProps<'div'> & {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function TabsContents({
|
|
children,
|
|
className,
|
|
transition = { type: 'spring', stiffness: 200, damping: 25 },
|
|
...props
|
|
}: TabsContentsProps) {
|
|
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;
|
|
if (!newHeight) return;
|
|
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 (
|
|
<motion.div
|
|
data-slot="tabs-contents"
|
|
layout
|
|
animate={{ height: height }}
|
|
transition={transition}
|
|
className={className}
|
|
{...props}
|
|
>
|
|
<div ref={containerRef}>{children}</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Tabs,
|
|
TabsList,
|
|
TabsTrigger,
|
|
TabsContent,
|
|
TabsContents,
|
|
type TabsProps,
|
|
type TabsListProps,
|
|
type TabsTriggerProps,
|
|
type TabsContentProps,
|
|
type TabsContentsProps,
|
|
};
|