1import { cva, type VariantProps } from 'class-variance-authority';
2import * as React from 'react';
3import { cn } from '../lib/cn';
4import { assignRef } from '../lib/overlay';
5
6const bubbleVariants = cva(
7 'relative flex w-fit max-w-full flex-col rounded-xl text-sm transition-colors',
8 {
9 variants: {
10 variant: {
11 default: 'bg-primary text-primary-foreground',
12 secondary: 'bg-secondary text-secondary-foreground',
13 muted: 'bg-muted text-foreground',
14 tinted: 'bg-primary/10 text-foreground',
15 outline: 'border-border text-foreground border bg-transparent',
16 ghost: 'text-foreground bg-transparent',
17 destructive:
18 'bg-destructive-surface text-destructive-text border-destructive-border border',
19 },
20 align: {
21 start: 'self-start',
22 end: 'self-end',
23 },
24 },
25 defaultVariants: {
26 variant: 'default',
27 align: 'start',
28 },
29 },
30);
31
32const bubbleReactionsVariants = cva(
33 'border-border bg-background absolute z-10 flex items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-xs leading-none shadow-sm select-none',
34 {
35 variants: {
36 side: {
37 top: '-top-3',
38 bottom: '-bottom-3',
39 },
40 align: {
41 start: 'left-2',
42 end: 'right-2',
43 },
44 },
45 defaultVariants: {
46 side: 'bottom',
47 align: 'end',
48 },
49 },
50);
51
52const bubbleGroupVariants = cva('flex w-full flex-col gap-1', {
53 variants: {
54 align: {
55 start: 'items-start',
56 end: 'items-end',
57 },
58 },
59 defaultVariants: {
60 align: 'start',
61 },
62});
63
64interface BubbleProps
65 extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof bubbleVariants> {}
66
67interface BubbleContentProps extends React.HTMLAttributes<HTMLDivElement> {
68 asChild?: boolean;
69}
70
71interface BubbleReactionsProps
72 extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof bubbleReactionsVariants> {}
73
74interface BubbleGroupProps
75 extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof bubbleGroupVariants> {}
76
77type SlotProps = Record<string, unknown> & {
78 className?: string;
79 style?: React.CSSProperties;
80};
81
82function mergeSlotProps(slotProps: SlotProps, childProps: SlotProps): SlotProps {
83 const merged: SlotProps = { ...slotProps, ...childProps };
84 for (const key of Object.keys(slotProps)) {
85 const slotValue = slotProps[key];
86 const childValue = childProps[key];
87 if (
88 /^on[A-Z]/.test(key) &&
89 typeof slotValue === 'function' &&
90 typeof childValue === 'function'
91 ) {
92 merged[key] = (...args: unknown[]) => {
93 childValue(...args);
94 slotValue(...args);
95 };
96 }
97 }
98 if (slotProps.style || childProps.style) {
99 merged.style = { ...slotProps.style, ...childProps.style };
100 }
101 merged.className = cn(slotProps.className, childProps.className);
102 return merged;
103}
104
105const BubbleRoot = ({
106 className,
107 variant,
108 align,
109 ref,
110 ...props
111}: BubbleProps & { ref?: React.Ref<HTMLDivElement> }) => {
112 return <div ref={ref} className={cn(bubbleVariants({ variant, align }), className)} {...props} />;
113};
114
115BubbleRoot.displayName = 'Bubble';
116
117const BubbleContent = ({
118 className,
119 asChild = false,
120 children,
121 ref,
122 ...props
123}: BubbleContentProps & { ref?: React.Ref<HTMLDivElement> }) => {
124 const contentClassName = cn(
125 'px-3.5 py-2 leading-relaxed break-words whitespace-pre-wrap',
126 className,
127 );
128
129 if (asChild && React.isValidElement(children)) {
130 const childProps = children.props as SlotProps & { ref?: React.Ref<HTMLElement> };
131 const childRef = childProps.ref;
132 return React.cloneElement(children as React.ReactElement<SlotProps>, {
133 ...mergeSlotProps({ ...props, className: contentClassName }, childProps),
134 ref:
135 ref || childRef
136 ? (node: HTMLDivElement | null) => {
137 assignRef(ref, node);
138 assignRef(childRef, node);
139 }
140 : undefined,
141 });
142 }
143
144 return (
145 <div ref={ref} className={contentClassName} {...props}>
146 {children}
147 </div>
148 );
149};
150
151BubbleContent.displayName = 'BubbleContent';
152
153const BubbleReactions = ({
154 className,
155 side,
156 align,
157 ref,
158 ...props
159}: BubbleReactionsProps & { ref?: React.Ref<HTMLDivElement> }) => {
160 return (
161 <div ref={ref} className={cn(bubbleReactionsVariants({ side, align }), className)} {...props} />
162 );
163};
164
165BubbleReactions.displayName = 'BubbleReactions';
166
167const BubbleGroup = ({
168 className,
169 align,
170 ref,
171 ...props
172}: BubbleGroupProps & { ref?: React.Ref<HTMLDivElement> }) => {
173 return <div ref={ref} className={cn(bubbleGroupVariants({ align }), className)} {...props} />;
174};
175
176BubbleGroup.displayName = 'BubbleGroup';
177
178const Bubble = Object.assign(BubbleRoot, {
179 Content: BubbleContent,
180 Reactions: BubbleReactions,
181 Group: BubbleGroup,
182});
183
184export { Bubble, bubbleGroupVariants, bubbleReactionsVariants, bubbleVariants };
185export type { BubbleContentProps, BubbleGroupProps, BubbleProps, BubbleReactionsProps };