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

160 lines
5.5 KiB
TypeScript

'use client';
import * as React from 'react';
import { cn } from '@workspace/ui/lib/utils';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
type TooltipProps,
type TooltipContentProps,
} from '@/registry/components/tooltip';
type Align = 'start' | 'center' | 'end';
type AvatarProps = TooltipProps & {
children: React.ReactNode;
align?: Align;
invertOverlap?: boolean;
};
function AvatarContainer({
children,
align,
invertOverlap,
...props
}: AvatarProps) {
return (
<Tooltip {...props}>
<TooltipTrigger>
<span
data-slot="avatar-container"
className={cn(
align === 'start'
? 'items-start'
: align === 'center'
? 'items-center'
: 'items-end',
'relative grid w-[var(--avatar-size)] aspect-[1/calc(1+var(--avatar-mask-ratio))]',
'[&_[data-slot=avatar]]:size-[var(--avatar-size)] [&_[data-slot=avatar]]:rounded-full',
invertOverlap
? cn(
'[&:not(:first-of-type)]:[--circle:calc(((var(--avatar-border)*2)+var(--avatar-size))*0.5)]',
'[&:not(:first-of-type)]:mask-[radial-gradient(var(--circle)_var(--circle)_at_calc(var(--circle)-var(--avatar-column-size)-var(--avatar-border))_50%_,#0000_calc(var(--circle)-0.5px),#fff_var(--circle))]',
'[&:not(:first-of-type)]:mask-size-[100%_100%]',
'[&:not(:first-of-type)]:mask-position-[0_calc(var(--avatar-size)*var(--avatar-mask-base))]',
'[&:not(:first-of-type)]:transition-[mask-position] [&:not(:first-of-type)]:duration-300 [&:not(:first-of-type)]:ease-in-out',
'[&:hover+&]:mask-position-[0_calc(var(--avatar-size)_-_calc(var(--avatar-size)*(var(--avatar-mask-factor)+var(--avatar-mask-offset))))]',
)
: cn(
'[&:not(:last-of-type)]:[--circle:calc(((var(--avatar-border)*2)+var(--avatar-size))*0.5)]',
'[&:not(:last-of-type)]:mask-[radial-gradient(var(--circle)_var(--circle)_at_calc(var(--circle)+var(--avatar-column-size)-var(--avatar-border))_50%_,#0000_calc(var(--circle)-0.5px),#fff_var(--circle))]',
'[&:not(:last-of-type)]:mask-size-[100%_100%]',
'[&:not(:last-of-type)]:mask-position-[0_calc(var(--avatar-size)*var(--avatar-mask-base))]',
'[&:not(:last-of-type)]:transition-[mask-position] [&:not(:last-of-type)]:duration-300 [&:not(:last-of-type)]:ease-in-out',
'[&:has(+&:hover)]:mask-position-[0_calc(var(--avatar-size)_-_calc(var(--avatar-size)*(var(--avatar-mask-factor)+var(--avatar-mask-offset))))]',
),
'[&>span]:transition-[translate] [&>span]:duration-300 [&>span]:ease-in-out',
'[&:hover_span:first-of-type]:translate-y-[var(--avatar-translate-pct)]',
)}
>
{children}
</span>
</TooltipTrigger>
</Tooltip>
);
}
type AvatarGroupTooltipProps = TooltipContentProps;
function AvatarGroupTooltip(props: AvatarGroupTooltipProps) {
return <TooltipContent {...props} />;
}
type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & {
children: React.ReactElement[];
invertOverlap?: boolean;
translate?: number;
size?: string | number;
border?: string | number;
columnSize?: string | number;
align?: Align;
tooltipProps?: Omit<TooltipProps, 'children'>;
};
function AvatarGroup({
ref,
children,
className,
invertOverlap = false,
size = '43px',
border = '3px',
columnSize = '37px',
align = 'end',
translate = -30,
tooltipProps = { side: 'top', sideOffset: 12 },
...props
}: AvatarGroupProps) {
const maskRatio = Math.abs(translate / 100);
const alignOffset =
align === 'start' ? 0 : align === 'center' ? maskRatio / 2 : maskRatio;
const maskBase = alignOffset - maskRatio / 2;
const maskFactor = 1 - alignOffset + maskRatio / 2;
return (
<TooltipProvider openDelay={0} closeDelay={0}>
<div
ref={ref}
data-slot="avatar-group"
style={
{
'--avatar-size': size,
'--avatar-border': border,
'--avatar-column-size': columnSize,
'--avatar-translate-pct': `${translate}%`,
'--avatar-mask-offset': -(translate / 100),
'--avatar-mask-ratio': maskRatio,
'--avatar-mask-base': maskBase,
'--avatar-mask-factor': maskFactor,
'--avatar-columns': React.Children.count(children),
} as React.CSSProperties
}
className="h-[var(--avatar-size)] w-[calc(var(--avatar-column-size)*(var(--avatar-columns))+calc(var(--avatar-size)-var(--avatar-column-size)))]"
>
<span
className={cn(
'grid h-[var(--avatar-size)] grid-cols-[repeat(var(--avatar-columns),var(--avatar-column-size))]',
align === 'start'
? 'content-start'
: align === 'center'
? 'content-center'
: 'content-end',
className,
)}
{...props}
>
{children?.map((child, index) => (
<AvatarContainer
key={index}
invertOverlap={invertOverlap}
{...tooltipProps}
align={align}
>
{child}
</AvatarContainer>
))}
</span>
</div>
</TooltipProvider>
);
}
export {
AvatarGroup,
AvatarGroupTooltip,
type AvatarGroupProps,
type AvatarGroupTooltipProps,
};