1import { cva, type VariantProps } from 'class-variance-authority';
2import * as React from 'react';
3import { cn } from '../lib/cn';
4
5const emptyVariants = cva(
6 'flex w-full flex-col items-center justify-center gap-4 rounded-xl px-6 py-10 text-center',
7 {
8 variants: {
9 variant: {
10 default: '',
11 outline: 'border-border border border-dashed',
12 card: 'bg-card border-border border',
13 },
14 },
15 defaultVariants: {
16 variant: 'default',
17 },
18 },
19);
20
21const emptyMediaVariants = cva('mb-1 flex items-center justify-center', {
22 variants: {
23 variant: {
24 default: '',
25 icon: 'bg-muted text-muted-foreground size-12 rounded-lg [&_svg]:size-6',
26 },
27 },
28 defaultVariants: {
29 variant: 'default',
30 },
31});
32
33interface EmptyProps
34 extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof emptyVariants> {}
35
36type EmptyHeaderProps = React.HTMLAttributes<HTMLDivElement>;
37
38interface EmptyMediaProps
39 extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof emptyMediaVariants> {}
40
41interface EmptyTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
42 as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
43}
44
45type EmptyDescriptionProps = React.HTMLAttributes<HTMLParagraphElement>;
46type EmptyContentProps = React.HTMLAttributes<HTMLDivElement>;
47
48const EmptyRoot = ({
49 className,
50 variant,
51 ref,
52 ...props
53}: EmptyProps & { ref?: React.Ref<HTMLDivElement> }) => {
54 return <div ref={ref} className={cn(emptyVariants({ variant }), className)} {...props} />;
55};
56
57EmptyRoot.displayName = 'Empty';
58
59const EmptyHeader = ({
60 className,
61 ref,
62 ...props
63}: EmptyHeaderProps & { ref?: React.Ref<HTMLDivElement> }) => {
64 return (
65 <div
66 ref={ref}
67 className={cn('flex max-w-sm flex-col items-center gap-2', className)}
68 {...props}
69 />
70 );
71};
72
73EmptyHeader.displayName = 'EmptyHeader';
74
75const EmptyMedia = ({
76 className,
77 variant,
78 ref,
79 ...props
80}: EmptyMediaProps & { ref?: React.Ref<HTMLDivElement> }) => {
81 return (
82 <div
83 ref={ref}
84 aria-hidden="true"
85 className={cn(emptyMediaVariants({ variant }), className)}
86 {...props}
87 />
88 );
89};
90
91EmptyMedia.displayName = 'EmptyMedia';
92
93const EmptyTitle = ({
94 className,
95 as: Component = 'h3',
96 ref,
97 ...props
98}: EmptyTitleProps & { ref?: React.Ref<HTMLHeadingElement> }) => {
99 return (
100 <Component
101 ref={ref}
102 className={cn('text-base font-medium tracking-tight', className)}
103 {...props}
104 />
105 );
106};
107
108EmptyTitle.displayName = 'EmptyTitle';
109
110const EmptyDescription = ({
111 className,
112 ref,
113 ...props
114}: EmptyDescriptionProps & { ref?: React.Ref<HTMLParagraphElement> }) => {
115 return (
116 <p
117 ref={ref}
118 className={cn(
119 'text-muted-foreground text-sm leading-relaxed',
120 '[&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4',
121 className,
122 )}
123 {...props}
124 />
125 );
126};
127
128EmptyDescription.displayName = 'EmptyDescription';
129
130const EmptyContent = ({
131 className,
132 ref,
133 ...props
134}: EmptyContentProps & { ref?: React.Ref<HTMLDivElement> }) => {
135 return (
136 <div
137 ref={ref}
138 className={cn('flex flex-wrap items-center justify-center gap-2', className)}
139 {...props}
140 />
141 );
142};
143
144EmptyContent.displayName = 'EmptyContent';
145
146const Empty = Object.assign(EmptyRoot, {
147 Header: EmptyHeader,
148 Media: EmptyMedia,
149 Title: EmptyTitle,
150 Description: EmptyDescription,
151 Content: EmptyContent,
152});
153
154export { Empty, emptyMediaVariants, emptyVariants };
155export type {
156 EmptyContentProps,
157 EmptyDescriptionProps,
158 EmptyHeaderProps,
159 EmptyMediaProps,
160 EmptyProps,
161 EmptyTitleProps,
162};