1"use client";
2
3import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
4import { XIcon } from "lucide-react";
5import type { CSSProperties } from "react";
6import { cn } from "~/lib/utils";
7import { Button } from "~/components/ui/button";
8
9const Sheet = SheetPrimitive.Root;
10
11const SheetPortal = SheetPrimitive.Portal;
12
13// Sheets are docked panels, not dialogs: their layer (--z-sheet) sits under dialogs that open
14// from inside them and under anything the app floats above panels.
15function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
16 return (
17 <SheetPrimitive.Backdrop
18 className={cn(
19 "fixed inset-0 z-(--z-sheet) bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
20 className,
21 )}
22 data-slot="sheet-backdrop"
23 {...props}
24 />
25 );
26}
27
28function SheetViewport({
29 className,
30 side,
31 variant = "default",
32 ...props
33}: SheetPrimitive.Viewport.Props & {
34 side?: "right" | "left" | "top" | "bottom";
35 variant?: "default" | "inset";
36}) {
37 return (
38 <SheetPrimitive.Viewport
39 className={cn(
40 "fixed inset-0 z-(--z-sheet) grid",
41 side === "bottom" && "grid grid-rows-[1fr_auto] pt-12",
42 side === "top" && "grid grid-rows-[auto_1fr] pb-12",
43 side === "left" && "flex justify-start",
44 side === "right" && "flex justify-end",
45 variant === "inset" && "sm:p-4",
46 className,
47 )}
48 data-slot="sheet-viewport"
49 {...props}
50 />
51 );
52}
53
54function SheetPopup({
55 className,
56 children,
57 showCloseButton = true,
58 keepMounted = false,
59 transitionDurationMs,
60 side = "right",
61 variant = "default",
62 style,
63 ...props
64}: SheetPrimitive.Popup.Props & {
65 showCloseButton?: boolean;
66 keepMounted?: boolean;
67 transitionDurationMs?: number;
68 side?: "right" | "left" | "top" | "bottom";
69 variant?: "default" | "inset";
70}) {
71 const transitionStyle =
72 transitionDurationMs === undefined
73 ? undefined
74 : ({ transitionDuration: `${transitionDurationMs}ms` } satisfies CSSProperties);
75 const instant = transitionDurationMs === 0;
76
77 return (
78 <SheetPortal keepMounted={keepMounted}>
79 <SheetBackdrop
80 className={
81 instant
82 ? "transition-none! data-ending-style:opacity-100! data-starting-style:opacity-100!"
83 : undefined
84 }
85 style={transitionStyle}
86 />
87 <SheetViewport side={side} variant={variant}>
88 <SheetPrimitive.Popup
89 className={cn(
90 "relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
91 side === "bottom" &&
92 "row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8",
93 side === "top" &&
94 "data-ending-style:-translate-y-8 data-starting-style:-translate-y-8 border-b",
95 side === "left" &&
96 "data-ending-style:-translate-x-8 data-starting-style:-translate-x-8 w-[calc(100%-(--spacing(12)))] max-w-md border-e",
97 side === "right" &&
98 "col-start-2 w-[calc(100%-(--spacing(12)))] max-w-md border-s data-ending-style:translate-x-8 data-starting-style:translate-x-8",
99 variant === "inset" &&
100 "before:hidden sm:rounded-2xl sm:border sm:before:rounded-[calc(var(--radius-2xl)-1px)] sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]",
101 instant &&
102 "transition-none! will-change-auto! data-ending-style:translate-x-0! data-starting-style:translate-x-0! data-ending-style:translate-y-0! data-starting-style:translate-y-0! data-ending-style:opacity-100! data-starting-style:opacity-100!",
103 className,
104 )}
105 data-slot="sheet-popup"
106 style={{ ...transitionStyle, ...style }}
107 {...props}
108 >
109 {children}
110 {showCloseButton && (
111 <SheetPrimitive.Close
112 aria-label="Close"
113 className="absolute end-2 top-2"
114 render={<Button size="icon" variant="ghost" />}
115 >
116 <XIcon />
117 </SheetPrimitive.Close>
118 )}
119 </SheetPrimitive.Popup>
120 </SheetViewport>
121 </SheetPortal>
122 );
123}
124
125function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
126 return (
127 <div
128 className={cn(
129 "flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
130 className,
131 )}
132 data-slot="sheet-header"
133 {...props}
134 />
135 );
136}
137
138function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
139 return (
140 <SheetPrimitive.Title
141 className={cn("font-semibold text-xl leading-none", className)}
142 data-slot="sheet-title"
143 {...props}
144 />
145 );
146}
147
148function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
149 return (
150 <SheetPrimitive.Description
151 className={cn("text-muted-foreground text-sm", className)}
152 data-slot="sheet-description"
153 {...props}
154 />
155 );
156}
157
158export {
159 Sheet,
160 SheetPortal,
161 SheetBackdrop,
162 SheetBackdrop as SheetOverlay,
163 SheetPopup,
164 SheetPopup as SheetContent,
165 SheetHeader,
166 SheetTitle,
167 SheetDescription,
168};