@patternmode/stacksheet 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/Drag/DragConstants.d.ts +9 -0
  4. package/dist/Drag/DragConstants.d.ts.map +1 -0
  5. package/dist/Drag/DragGeometry.d.ts +34 -0
  6. package/dist/Drag/DragGeometry.d.ts.map +1 -0
  7. package/dist/Drag/DragTypes.d.ts +34 -0
  8. package/dist/Drag/DragTypes.d.ts.map +1 -0
  9. package/dist/Drag/index.d.ts +3 -0
  10. package/dist/Drag/index.d.ts.map +1 -0
  11. package/dist/Drag/useDrag.d.ts +20 -0
  12. package/dist/Drag/useDrag.d.ts.map +1 -0
  13. package/dist/SheetPanel/DefaultHeader.d.ts +5 -0
  14. package/dist/SheetPanel/DefaultHeader.d.ts.map +1 -0
  15. package/dist/SheetPanel/SheetPanel.d.ts +3 -0
  16. package/dist/SheetPanel/SheetPanel.d.ts.map +1 -0
  17. package/dist/SheetPanel/SheetPanelContent.d.ts +13 -0
  18. package/dist/SheetPanel/SheetPanelContent.d.ts.map +1 -0
  19. package/dist/SheetPanel/SheetPanelFocus.d.ts +8 -0
  20. package/dist/SheetPanel/SheetPanelFocus.d.ts.map +1 -0
  21. package/dist/SheetPanel/SheetPanelHandles.d.ts +10 -0
  22. package/dist/SheetPanel/SheetPanelHandles.d.ts.map +1 -0
  23. package/dist/SheetPanel/SheetPanelTypes.d.ts +31 -0
  24. package/dist/SheetPanel/SheetPanelTypes.d.ts.map +1 -0
  25. package/dist/SheetPanel/index.d.ts +3 -0
  26. package/dist/SheetPanel/index.d.ts.map +1 -0
  27. package/dist/SheetParts/SheetBack.d.ts +3 -0
  28. package/dist/SheetParts/SheetBack.d.ts.map +1 -0
  29. package/dist/SheetParts/SheetBody.d.ts +3 -0
  30. package/dist/SheetParts/SheetBody.d.ts.map +1 -0
  31. package/dist/SheetParts/SheetClose.d.ts +3 -0
  32. package/dist/SheetParts/SheetClose.d.ts.map +1 -0
  33. package/dist/SheetParts/SheetDescription.d.ts +3 -0
  34. package/dist/SheetParts/SheetDescription.d.ts.map +1 -0
  35. package/dist/SheetParts/SheetFooter.d.ts +3 -0
  36. package/dist/SheetParts/SheetFooter.d.ts.map +1 -0
  37. package/dist/SheetParts/SheetHandle.d.ts +3 -0
  38. package/dist/SheetParts/SheetHandle.d.ts.map +1 -0
  39. package/dist/SheetParts/SheetHeader.d.ts +3 -0
  40. package/dist/SheetParts/SheetHeader.d.ts.map +1 -0
  41. package/dist/SheetParts/SheetPartTypes.d.ts +22 -0
  42. package/dist/SheetParts/SheetPartTypes.d.ts.map +1 -0
  43. package/dist/SheetParts/SheetParts.d.ts +28 -0
  44. package/dist/SheetParts/SheetParts.d.ts.map +1 -0
  45. package/dist/SheetParts/SheetTitle.d.ts +3 -0
  46. package/dist/SheetParts/SheetTitle.d.ts.map +1 -0
  47. package/dist/SheetParts/index.d.ts +11 -0
  48. package/dist/SheetParts/index.d.ts.map +1 -0
  49. package/dist/Store/StoreArgs.d.ts +15 -0
  50. package/dist/Store/StoreArgs.d.ts.map +1 -0
  51. package/dist/Store/StoreTypes.d.ts +21 -0
  52. package/dist/Store/StoreTypes.d.ts.map +1 -0
  53. package/dist/Store/createSheetStore.d.ts +15 -0
  54. package/dist/Store/createSheetStore.d.ts.map +1 -0
  55. package/dist/Store/index.d.ts +3 -0
  56. package/dist/Store/index.d.ts.map +1 -0
  57. package/dist/config.d.ts +4 -0
  58. package/dist/config.d.ts.map +1 -0
  59. package/dist/create.d.ts +33 -0
  60. package/dist/create.d.ts.map +1 -0
  61. package/dist/icons.d.ts +4 -0
  62. package/dist/icons.d.ts.map +1 -0
  63. package/dist/index.d.ts +10 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.mjs +1732 -0
  66. package/dist/index.mjs.map +1 -0
  67. package/dist/media.d.ts +9 -0
  68. package/dist/media.d.ts.map +1 -0
  69. package/dist/panel-context.d.ts +26 -0
  70. package/dist/panel-context.d.ts.map +1 -0
  71. package/dist/parts.d.ts +11 -0
  72. package/dist/parts.d.ts.map +1 -0
  73. package/dist/renderer-effects.d.ts +6 -0
  74. package/dist/renderer-effects.d.ts.map +1 -0
  75. package/dist/renderer-helpers.d.ts +53 -0
  76. package/dist/renderer-helpers.d.ts.map +1 -0
  77. package/dist/renderer.d.ts +29 -0
  78. package/dist/renderer.d.ts.map +1 -0
  79. package/dist/sheet-panel.d.ts +3 -0
  80. package/dist/sheet-panel.d.ts.map +1 -0
  81. package/dist/snap-points.d.ts +22 -0
  82. package/dist/snap-points.d.ts.map +1 -0
  83. package/dist/springs.d.ts +26 -0
  84. package/dist/springs.d.ts.map +1 -0
  85. package/dist/stacking.d.ts +38 -0
  86. package/dist/stacking.d.ts.map +1 -0
  87. package/dist/store.d.ts +3 -0
  88. package/dist/store.d.ts.map +1 -0
  89. package/dist/styles.css +2 -0
  90. package/dist/types.d.ts +232 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/use-drag.d.ts +3 -0
  93. package/dist/use-drag.d.ts.map +1 -0
  94. package/package.json +90 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1732 @@
1
+ "use client";
2
+ import { Portal } from "@radix-ui/react-portal";
3
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { createStore, useStore } from "zustand";
5
+ import { useShallow } from "zustand/react/shallow";
6
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
7
+ import { RemoveScroll } from "react-remove-scroll";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { FocusTrap } from "focus-trap-react";
10
+ import { joinClassNames } from "@patternmode/system";
11
+ import { Slot } from "@radix-ui/react-slot";
12
+ import { Root, Scrollbar, Thumb, Viewport } from "@radix-ui/react-scroll-area";
13
+ //#region src/springs.ts
14
+ /**
15
+ * Spring presets inspired by iOS animation feel.
16
+ *
17
+ * - `subtle` — Barely noticeable bounce, professional.
18
+ * - `snappy` — Quick, responsive for interactions.
19
+ * - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
20
+ */
21
+ const springs = {
22
+ subtle: {
23
+ stiffness: 300,
24
+ damping: 30,
25
+ mass: 1
26
+ },
27
+ snappy: {
28
+ stiffness: 400,
29
+ damping: 28,
30
+ mass: .8
31
+ },
32
+ stiff: {
33
+ stiffness: 400,
34
+ damping: 40,
35
+ mass: 1
36
+ }
37
+ };
38
+ //#endregion
39
+ //#region src/config.ts
40
+ const DEFAULT_STACKING = {
41
+ scaleStep: .04,
42
+ offsetStep: 16,
43
+ opacityStep: 0,
44
+ radius: 12,
45
+ renderThreshold: 3
46
+ };
47
+ const DEFAULT_SIDE = {
48
+ desktop: "right",
49
+ mobile: "bottom"
50
+ };
51
+ /** Normalize a string side to a responsive object, merging with defaults. */
52
+ function resolveSide(side) {
53
+ if (typeof side === "string") return {
54
+ desktop: side,
55
+ mobile: side
56
+ };
57
+ return {
58
+ ...DEFAULT_SIDE,
59
+ ...side
60
+ };
61
+ }
62
+ /** Resolve a preset name to a SpringConfig, or merge partial config with defaults. */
63
+ function resolveSpring(spring) {
64
+ if (typeof spring === "string") return springs[spring];
65
+ return {
66
+ ...springs.stiff,
67
+ ...spring
68
+ };
69
+ }
70
+ /** Merge user-provided config with defaults. Resolves union types (side, spring) to concrete values. */
71
+ function resolveConfig(config = {}) {
72
+ return {
73
+ maxDepth: config.maxDepth ?? Number.POSITIVE_INFINITY,
74
+ closeOnEscape: config.closeOnEscape ?? true,
75
+ closeOnBackdrop: config.closeOnBackdrop ?? true,
76
+ showOverlay: config.showOverlay ?? true,
77
+ lockScroll: config.lockScroll ?? true,
78
+ width: config.width ?? 420,
79
+ maxWidth: config.maxWidth ?? "90vw",
80
+ breakpoint: config.breakpoint ?? 768,
81
+ side: resolveSide(config.side),
82
+ stacking: {
83
+ ...DEFAULT_STACKING,
84
+ ...config.stacking
85
+ },
86
+ spring: resolveSpring(config.spring),
87
+ zIndex: config.zIndex ?? 100,
88
+ ariaLabel: config.ariaLabel ?? "Sheet dialog",
89
+ onOpenComplete: config.onOpenComplete,
90
+ onCloseComplete: config.onCloseComplete,
91
+ snapPoints: config.snapPoints ?? [],
92
+ snapPointIndex: config.snapPointIndex,
93
+ onSnapPointChange: config.onSnapPointChange,
94
+ snapToSequentialPoints: config.snapToSequentialPoints ?? false,
95
+ drag: config.drag ?? true,
96
+ closeThreshold: config.closeThreshold ?? .25,
97
+ velocityThreshold: config.velocityThreshold ?? .5,
98
+ dismissible: config.dismissible ?? true,
99
+ modal: config.modal ?? true,
100
+ shouldScaleBackground: config.shouldScaleBackground ?? false,
101
+ scaleBackgroundAmount: config.scaleBackgroundAmount ?? .97
102
+ };
103
+ }
104
+ //#endregion
105
+ //#region src/media.ts
106
+ /**
107
+ * Returns true when viewport width is at or below the breakpoint.
108
+ * SSR-safe: defaults to false (desktop).
109
+ */
110
+ function useIsMobile(breakpoint) {
111
+ const [isMobile, setIsMobile] = useState(false);
112
+ useEffect(() => {
113
+ const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
114
+ setIsMobile(mql.matches);
115
+ const handler = (e) => setIsMobile(e.matches);
116
+ mql.addEventListener("change", handler);
117
+ return () => mql.removeEventListener("change", handler);
118
+ }, [breakpoint]);
119
+ return isMobile;
120
+ }
121
+ /** Resolve the current side from config + viewport. */
122
+ function useResolvedSide(config) {
123
+ return useIsMobile(config.breakpoint) ? config.side.mobile : config.side.desktop;
124
+ }
125
+ //#endregion
126
+ //#region src/renderer-effects.ts
127
+ function usePanelHeight(panelRef, hasSnapPoints) {
128
+ const [height, setHeight] = useState(0);
129
+ useEffect(() => {
130
+ const el = panelRef.current;
131
+ if (!(el && hasSnapPoints)) return;
132
+ setHeight(el.offsetHeight);
133
+ const observer = new ResizeObserver(([entry]) => {
134
+ if (entry) setHeight(entry.contentRect.height);
135
+ });
136
+ observer.observe(el);
137
+ return () => observer.disconnect();
138
+ }, [panelRef, hasSnapPoints]);
139
+ return height;
140
+ }
141
+ function useViewportHeight(active) {
142
+ const getHeight = useCallback(() => typeof window === "undefined" ? 0 : window.visualViewport?.height ?? window.innerHeight, []);
143
+ const [height, setHeight] = useState(() => getHeight());
144
+ useEffect(() => {
145
+ if (!active || typeof window === "undefined") return;
146
+ const update = () => setHeight(getHeight());
147
+ update();
148
+ window.addEventListener("resize", update);
149
+ window.visualViewport?.addEventListener("resize", update);
150
+ return () => {
151
+ window.removeEventListener("resize", update);
152
+ window.visualViewport?.removeEventListener("resize", update);
153
+ };
154
+ }, [active, getHeight]);
155
+ return height;
156
+ }
157
+ function useBodyScale(config, isOpen, prefersReducedMotion) {
158
+ useEffect(() => {
159
+ if (!config.shouldScaleBackground || prefersReducedMotion) return;
160
+ const wrapper = document.querySelector("[data-stacksheet-wrapper]");
161
+ if (!(wrapper && wrapper instanceof HTMLElement)) return;
162
+ if (isOpen) {
163
+ const scale = config.scaleBackgroundAmount;
164
+ wrapper.style.transition = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
165
+ wrapper.style.transform = `scale(${scale})`;
166
+ wrapper.style.borderRadius = "8px";
167
+ wrapper.style.overflow = "hidden";
168
+ wrapper.style.transformOrigin = "center top";
169
+ return;
170
+ }
171
+ wrapper.style.transform = "";
172
+ wrapper.style.borderRadius = "";
173
+ const handleEnd = () => {
174
+ wrapper.style.transition = "";
175
+ wrapper.style.overflow = "";
176
+ wrapper.style.transformOrigin = "";
177
+ };
178
+ wrapper.addEventListener("transitionend", handleEnd, { once: true });
179
+ return () => wrapper.removeEventListener("transitionend", handleEnd);
180
+ }, [
181
+ isOpen,
182
+ config.shouldScaleBackground,
183
+ config.scaleBackgroundAmount,
184
+ prefersReducedMotion
185
+ ]);
186
+ }
187
+ //#endregion
188
+ //#region src/snap-points.ts
189
+ const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/;
190
+ /**
191
+ * Resolve a snap point value to pixels given the viewport height.
192
+ * - number 0-1: fraction of viewport (0.5 → 50% of vh)
193
+ * - number > 1: pixel value (300 → 300px)
194
+ * - string: parsed via regex for CSS unit support
195
+ */
196
+ function resolveSnapPointPx(point, viewportHeight) {
197
+ if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
198
+ if (typeof point === "string") {
199
+ const match = point.match(SNAP_POINT_RE);
200
+ if (!match?.[1]) return 0;
201
+ const value = Number.parseFloat(match[1]);
202
+ switch (match[2]) {
203
+ case "px": return value;
204
+ case "rem":
205
+ case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
206
+ case "vh":
207
+ case "%": return value / 100 * viewportHeight;
208
+ default: return 0;
209
+ }
210
+ }
211
+ return 0;
212
+ }
213
+ /**
214
+ * Resolve all snap points to sorted pixel heights (ascending).
215
+ * Returns an array of heights in px that the drawer should snap to.
216
+ */
217
+ function resolveSnapPoints(points, viewportHeight) {
218
+ if (points.length === 0) return [];
219
+ const resolved = points.map((p) => resolveSnapPointPx(p, viewportHeight)).filter((px) => px > 0);
220
+ resolved.sort((a, b) => a - b);
221
+ const deduped = [];
222
+ for (const px of resolved) {
223
+ const last = deduped.at(-1);
224
+ if (last === void 0 || Math.abs(px - last) > 1) deduped.push(px);
225
+ }
226
+ return deduped;
227
+ }
228
+ /** Velocity threshold (px/ms) for skipping intermediate snap points */
229
+ const SNAP_VELOCITY_THRESHOLD = .4;
230
+ /** Max velocity to consider for snap offset calculation */
231
+ const MAX_SNAP_VELOCITY = 2;
232
+ /** Multiplier to convert velocity to pixel offset for snap target */
233
+ const SNAP_VELOCITY_MULTIPLIER = 150;
234
+ /**
235
+ * Given the current drag offset (from fully open), resolved snap heights,
236
+ * the panel height, and release velocity, find the best snap point index.
237
+ *
238
+ * `dragOffset` is positive in the dismiss direction (downward for bottom sheets).
239
+ * Snap heights are "how tall the drawer should be" (ascending order).
240
+ *
241
+ * Returns -1 if the gesture indicates full dismissal.
242
+ */
243
+ function findSnapTarget(dragOffset, panelHeight, snapHeights, velocity, currentIndex, sequential) {
244
+ if (snapHeights.length === 0) return -1;
245
+ const snapOffsets = snapHeights.map((h) => panelHeight - h);
246
+ const currentPos = dragOffset;
247
+ if (sequential) {
248
+ const nextIndex = currentIndex - (velocity > 0 ? 1 : -1);
249
+ if (nextIndex < 0) return -1;
250
+ if (nextIndex >= snapHeights.length) return snapHeights.length - 1;
251
+ return nextIndex;
252
+ }
253
+ const projectedPos = currentPos + (Math.abs(velocity) >= SNAP_VELOCITY_THRESHOLD ? Math.min(Math.max(velocity, -2), MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0);
254
+ const first = snapOffsets[0] ?? 0;
255
+ let bestIndex = 0;
256
+ let bestDist = Math.abs(projectedPos - first);
257
+ for (let i = 1; i < snapOffsets.length; i++) {
258
+ const offset = snapOffsets[i] ?? 0;
259
+ const dist = Math.abs(projectedPos - offset);
260
+ if (dist < bestDist) {
261
+ bestDist = dist;
262
+ bestIndex = i;
263
+ }
264
+ }
265
+ if (Math.abs(projectedPos - panelHeight) <= bestDist) return -1;
266
+ return bestIndex;
267
+ }
268
+ /**
269
+ * Get the Y offset for a snap point relative to fully open (0 offset).
270
+ * Returns the number of pixels the drawer should be translated down from fully open.
271
+ */
272
+ function getSnapOffset(snapIndex, snapHeights, panelHeight) {
273
+ if (snapIndex < 0 || snapIndex >= snapHeights.length) return 0;
274
+ return panelHeight - (snapHeights[snapIndex] ?? 0);
275
+ }
276
+ //#endregion
277
+ //#region src/renderer-helpers.ts
278
+ const EMPTY_CLASSNAMES = {
279
+ backdrop: "",
280
+ panel: "",
281
+ header: ""
282
+ };
283
+ function resolveClassNames(cn) {
284
+ if (!cn) return EMPTY_CLASSNAMES;
285
+ return {
286
+ backdrop: cn.backdrop ?? "",
287
+ panel: cn.panel ?? "",
288
+ header: cn.header ?? ""
289
+ };
290
+ }
291
+ function buildAriaProps(isTop, isModal, isComposable, ariaLabel, panelId, hasDescription) {
292
+ if (!isTop) return {};
293
+ const props = { role: "dialog" };
294
+ if (isModal) props["aria-modal"] = "true";
295
+ if (isComposable) {
296
+ props["aria-labelledby"] = `${panelId}-title`;
297
+ if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
298
+ } else props["aria-label"] = ariaLabel;
299
+ return props;
300
+ }
301
+ function getDragTransform(side, offset) {
302
+ if (offset === 0) return {};
303
+ switch (side) {
304
+ case "right": return { x: offset };
305
+ case "left": return { x: -offset };
306
+ case "bottom": return { y: offset };
307
+ default: return {};
308
+ }
309
+ }
310
+ const VISUAL_TWEEN = {
311
+ type: "tween",
312
+ duration: .25,
313
+ ease: "easeOut"
314
+ };
315
+ function buildPanelStyle(panelStyles, isTop, hasPanelClass, isDragging) {
316
+ return {
317
+ ...panelStyles,
318
+ pointerEvents: isTop ? "auto" : "none",
319
+ ...isTop ? {} : { contain: "layout style paint" },
320
+ ...isDragging ? { transition: "none" } : {},
321
+ ...hasPanelClass ? {} : {
322
+ background: "var(--background, #fff)",
323
+ borderColor: "var(--border, transparent)"
324
+ }
325
+ };
326
+ }
327
+ function buildPanelTransition(isDragging, isTop, spring, stackSpring) {
328
+ if (isDragging) return {
329
+ type: "tween",
330
+ duration: 0
331
+ };
332
+ return {
333
+ ...isTop ? spring : stackSpring,
334
+ borderRadius: VISUAL_TWEEN,
335
+ boxShadow: VISUAL_TWEEN
336
+ };
337
+ }
338
+ function computeSnapYOffset(side, snapHeights, activeSnapIndex, measuredHeight) {
339
+ if (side !== "bottom" || snapHeights.length === 0 || measuredHeight <= 0) return 0;
340
+ return getSnapOffset(activeSnapIndex, snapHeights, measuredHeight);
341
+ }
342
+ function getBottomSlideDistance(measuredHeight) {
343
+ if (measuredHeight > 0) return measuredHeight;
344
+ if (typeof window !== "undefined") return window.innerHeight;
345
+ return 1e3;
346
+ }
347
+ function resolveSlideFrom(side, slideFrom, measuredHeight) {
348
+ if (side !== "bottom") return slideFrom;
349
+ return { y: getBottomSlideDistance(measuredHeight) };
350
+ }
351
+ function buildAnimateTarget(slideTarget, stackOffset, dragOffset, transform, animatedRadius, transition, snapYOffset, isTop) {
352
+ const base = {
353
+ ...slideTarget,
354
+ ...stackOffset,
355
+ ...dragOffset,
356
+ scale: transform.scale,
357
+ opacity: transform.opacity,
358
+ ...animatedRadius,
359
+ boxShadow: getShadow(!isTop),
360
+ transition
361
+ };
362
+ if (snapYOffset > 0) return {
363
+ ...base,
364
+ y: (dragOffset.y ?? 0) + snapYOffset
365
+ };
366
+ return base;
367
+ }
368
+ function getInitialRadius(side) {
369
+ if (side === "bottom") return {
370
+ borderTopLeftRadius: 0,
371
+ borderTopRightRadius: 0,
372
+ borderBottomLeftRadius: 0,
373
+ borderBottomRightRadius: 0
374
+ };
375
+ return { borderRadius: 0 };
376
+ }
377
+ const SHADOW_SM = "0px 1px 3px 0px rgba(0,0,0,0.06), 0px 6px 12px 0px rgba(0,0,0,0.06)";
378
+ const SHADOW_LG = "0px 8px 24px 0px rgba(0,0,0,0.06), 0px 24px 48px 0px rgba(0,0,0,0.04), 0px 48px 96px 0px rgba(0,0,0,0.03)";
379
+ function getShadow(isNested) {
380
+ return isNested ? SHADOW_SM : SHADOW_LG;
381
+ }
382
+ //#endregion
383
+ //#region src/Drag/DragConstants.ts
384
+ /** Elements that should never initiate a drag */
385
+ const INTERACTIVE_TAGS = new Set([
386
+ "INPUT",
387
+ "TEXTAREA",
388
+ "SELECT",
389
+ "BUTTON",
390
+ "A"
391
+ ]);
392
+ /** Rubber-band resistance factor for dragging past resting position */
393
+ const RUBBER_BAND_FACTOR = .6;
394
+ //#endregion
395
+ //#region src/Drag/DragGeometry.ts
396
+ function isInteractiveElement(el) {
397
+ if (INTERACTIVE_TAGS.has(el.tagName)) return true;
398
+ if (el.isContentEditable) return true;
399
+ if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
400
+ if (el.closest("[data-stacksheet-no-drag]")) return true;
401
+ return false;
402
+ }
403
+ /**
404
+ * Walk up from `el` to find the nearest scrollable ancestor.
405
+ * Returns null if nothing is scrollable in the dismiss axis.
406
+ */
407
+ function findScrollableAncestor(el, axis) {
408
+ let current = el;
409
+ while (current) {
410
+ if (current instanceof HTMLElement) {
411
+ const style = getComputedStyle(current);
412
+ const overflow = axis === "y" ? style.overflowY : style.overflowX;
413
+ if (overflow === "auto" || overflow === "scroll") {
414
+ if (axis === "y" ? current.scrollHeight > current.clientHeight : current.scrollWidth > current.clientWidth) return current;
415
+ }
416
+ }
417
+ current = current.parentElement;
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * Check if a scrollable element is at its edge in the dismiss direction.
423
+ * For bottom sheets (sign=1, axis=y), "at edge" means scrolled to top.
424
+ * For left panels (sign=-1, axis=x), "at edge" means scrolled to right end.
425
+ */
426
+ function isAtScrollEdge(el, axis, sign) {
427
+ if (axis === "y") return sign === 1 ? el.scrollTop <= 0 : el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
428
+ return sign === 1 ? el.scrollLeft <= 0 : el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
429
+ }
430
+ /**
431
+ * Get the dismiss direction axis and sign for a given side.
432
+ * - right panel → dismiss by dragging right (+x)
433
+ * - left panel → dismiss by dragging left (-x)
434
+ * - bottom panel → dismiss by dragging down (+y)
435
+ */
436
+ function getDismissAxis(side) {
437
+ switch (side) {
438
+ case "right": return {
439
+ axis: "x",
440
+ sign: 1
441
+ };
442
+ case "left": return {
443
+ axis: "x",
444
+ sign: -1
445
+ };
446
+ case "bottom": return {
447
+ axis: "y",
448
+ sign: 1
449
+ };
450
+ default: return {
451
+ axis: "x",
452
+ sign: 1
453
+ };
454
+ }
455
+ }
456
+ /**
457
+ * Decide whether a gesture past the dead zone qualifies as a dismiss drag.
458
+ * Returns "drag" if it's a valid dismiss gesture, "none" if it's off-axis
459
+ * or moving in the wrong direction.
460
+ */
461
+ function classifyGesture(dx, dy, axis, sign) {
462
+ const absDx = Math.abs(dx);
463
+ const absDy = Math.abs(dy);
464
+ let angleDeg;
465
+ if (axis === "y") angleDeg = absDy === 0 ? 90 : Math.atan(absDx / absDy) * 180 / Math.PI;
466
+ else angleDeg = absDx === 0 ? 90 : Math.atan(absDy / absDx) * 180 / Math.PI;
467
+ if (angleDeg > 35) return "none";
468
+ if ((axis === "x" ? dx : dy) * sign < 0) return "none";
469
+ return "drag";
470
+ }
471
+ /** Decide whether a pointer gesture commits as a drag or should be ignored. */
472
+ function commitGesture(dx, dy, axis, sign, scrollEl) {
473
+ if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
474
+ if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
475
+ return "drag";
476
+ }
477
+ function getPanelDimension(panel, axis) {
478
+ if (!panel) return 300;
479
+ return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
480
+ }
481
+ //#endregion
482
+ //#region src/Drag/useDrag.ts
483
+ /**
484
+ * Hook that manages drag gestures that can dismiss a sheet panel.
485
+ *
486
+ * Gesture pipeline:
487
+ * 1. Dead zone (10px) — ignores micro-movements
488
+ * 2. Angle check (35°) — must be roughly aligned with dismiss axis
489
+ * 3. Scroll conflict — yields to scrollable containers not at edge
490
+ * 4. Commit — drag is active, applies offset via `onDragUpdate`
491
+ * 5. Release — velocity + threshold determine close/snap/bounce-back
492
+ *
493
+ * Opposite-direction drag uses √(offset) damping for elastic
494
+ * rubber-band resistance (same physics as iOS over-scroll).
495
+ *
496
+ * When `snapHeights` is provided, release targeting uses
497
+ * `findSnapTarget()` instead of the simple threshold check.
498
+ */
499
+ function useDrag(panelRef, config, onDragUpdate) {
500
+ const startRef = useRef(null);
501
+ const committedRef = useRef(null);
502
+ const offsetRef = useRef(0);
503
+ const scrollTargetRef = useRef(null);
504
+ const { axis, sign } = getDismissAxis(config.side);
505
+ const handlePointerDown = useCallback((e) => {
506
+ if (!config.enabled) return;
507
+ if (e.button !== 0) return;
508
+ const target = e.target;
509
+ if (!target) return;
510
+ const isHandle = !!target.closest("[data-stacksheet-handle]");
511
+ if (!isHandle && isInteractiveElement(target)) return;
512
+ scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
513
+ startRef.current = {
514
+ x: e.clientX,
515
+ y: e.clientY,
516
+ time: Date.now()
517
+ };
518
+ committedRef.current = null;
519
+ offsetRef.current = 0;
520
+ e.currentTarget?.setPointerCapture?.(e.pointerId);
521
+ }, [config.enabled, axis]);
522
+ const handlePointerMove = useCallback((e) => {
523
+ if (!startRef.current) return;
524
+ const dx = e.clientX - startRef.current.x;
525
+ const dy = e.clientY - startRef.current.y;
526
+ const dist = Math.sqrt(dx * dx + dy * dy);
527
+ if (committedRef.current === null && dist < 10) return;
528
+ if (committedRef.current === null) {
529
+ committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
530
+ if (committedRef.current !== "drag") {
531
+ startRef.current = null;
532
+ return;
533
+ }
534
+ }
535
+ if (committedRef.current !== "drag") return;
536
+ const directional = (axis === "x" ? dx : dy) * sign;
537
+ const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
538
+ offsetRef.current = clampedOffset;
539
+ onDragUpdate({
540
+ offset: clampedOffset,
541
+ isDragging: true
542
+ });
543
+ e.preventDefault();
544
+ }, [
545
+ axis,
546
+ sign,
547
+ onDragUpdate
548
+ ]);
549
+ const dismiss = useCallback(() => {
550
+ if (config.isNested) config.onPop();
551
+ else config.onClose();
552
+ }, [config]);
553
+ const handlePointerUp = useCallback((_e) => {
554
+ if (!startRef.current || committedRef.current !== "drag") {
555
+ startRef.current = null;
556
+ committedRef.current = null;
557
+ scrollTargetRef.current = null;
558
+ return;
559
+ }
560
+ const offset = Math.max(0, offsetRef.current);
561
+ const elapsed = Date.now() - startRef.current.time;
562
+ const velocity = elapsed > 0 ? offset / elapsed : 0;
563
+ startRef.current = null;
564
+ committedRef.current = null;
565
+ offsetRef.current = 0;
566
+ scrollTargetRef.current = null;
567
+ const panelSize = getPanelDimension(panelRef.current, axis);
568
+ if (config.snapHeights.length > 0) {
569
+ const targetIndex = findSnapTarget(offset, panelSize, config.snapHeights, velocity, config.activeSnapIndex, config.sequential);
570
+ if (targetIndex === -1) dismiss();
571
+ else {
572
+ config.onSnap(targetIndex);
573
+ onDragUpdate({
574
+ offset: 0,
575
+ isDragging: false
576
+ });
577
+ }
578
+ return;
579
+ }
580
+ const pastThreshold = offset / panelSize > config.closeThreshold;
581
+ const fastEnough = velocity > config.velocityThreshold;
582
+ if (pastThreshold || fastEnough) dismiss();
583
+ else onDragUpdate({
584
+ offset: 0,
585
+ isDragging: false
586
+ });
587
+ }, [
588
+ panelRef,
589
+ axis,
590
+ config,
591
+ onDragUpdate,
592
+ dismiss
593
+ ]);
594
+ const handlePointerCancel = useCallback(() => {
595
+ startRef.current = null;
596
+ committedRef.current = null;
597
+ offsetRef.current = 0;
598
+ scrollTargetRef.current = null;
599
+ onDragUpdate({
600
+ offset: 0,
601
+ isDragging: false
602
+ });
603
+ }, [onDragUpdate]);
604
+ useEffect(() => {
605
+ const el = panelRef.current;
606
+ if (!(el && config.enabled)) return;
607
+ el.addEventListener("pointerdown", handlePointerDown);
608
+ el.addEventListener("pointermove", handlePointerMove);
609
+ el.addEventListener("pointerup", handlePointerUp);
610
+ el.addEventListener("pointercancel", handlePointerCancel);
611
+ return () => {
612
+ el.removeEventListener("pointerdown", handlePointerDown);
613
+ el.removeEventListener("pointermove", handlePointerMove);
614
+ el.removeEventListener("pointerup", handlePointerUp);
615
+ el.removeEventListener("pointercancel", handlePointerCancel);
616
+ };
617
+ }, [
618
+ panelRef,
619
+ config.enabled,
620
+ handlePointerDown,
621
+ handlePointerMove,
622
+ handlePointerUp,
623
+ handlePointerCancel
624
+ ]);
625
+ }
626
+ //#endregion
627
+ //#region src/panel-context.tsx
628
+ const SheetPanelContext = createContext(null);
629
+ /**
630
+ * Access the current sheet panel's context.
631
+ * Must be called inside a component rendered by the sheet stack.
632
+ */
633
+ function useSheetPanel() {
634
+ const ctx = useContext(SheetPanelContext);
635
+ if (!ctx) throw new Error("Sheet.* components must be used inside a sheet panel. They should be rendered by a component opened via actions.open(), push(), etc.");
636
+ return ctx;
637
+ }
638
+ //#endregion
639
+ //#region src/stacking.ts
640
+ /**
641
+ * Compute visual transforms for a panel at a given depth.
642
+ * depth=0 is the top (foreground) panel.
643
+ * Panels beyond renderThreshold are clamped to the edge position and faded out.
644
+ */
645
+ function getStackTransform(depth, stacking) {
646
+ if (depth <= 0) return {
647
+ scale: 1,
648
+ offset: 0,
649
+ opacity: 1,
650
+ borderRadius: 0
651
+ };
652
+ const beyondThreshold = depth >= stacking.renderThreshold;
653
+ const visualDepth = beyondThreshold ? stacking.renderThreshold - 1 : depth;
654
+ return {
655
+ scale: Math.max(.5, 1 - visualDepth * stacking.scaleStep),
656
+ offset: visualDepth * stacking.offsetStep,
657
+ opacity: beyondThreshold ? 0 : Math.max(0, 1 - visualDepth * stacking.opacityStep),
658
+ borderRadius: stacking.radius
659
+ };
660
+ }
661
+ /**
662
+ * Border radius values for the animate target.
663
+ * Must be animated (not static CSS) so Motion applies scale correction
664
+ * when panels are scaled. See: https://motion.dev/docs/react-layout-animations#scale-correction
665
+ */
666
+ function getAnimatedBorderRadius(side, depth, stacking) {
667
+ if (side === "bottom") {
668
+ const radius = depth > 0 ? stacking.radius : 16;
669
+ return {
670
+ borderTopLeftRadius: radius,
671
+ borderTopRightRadius: radius,
672
+ borderBottomLeftRadius: 0,
673
+ borderBottomRightRadius: 0
674
+ };
675
+ }
676
+ if (depth > 0) return { borderRadius: stacking.radius };
677
+ return { borderRadius: 0 };
678
+ }
679
+ /** Motion initial/exit values for sliding from the given side. */
680
+ function getSlideFrom(side) {
681
+ switch (side) {
682
+ case "right": return { x: "100%" };
683
+ case "left": return { x: "-100%" };
684
+ case "bottom": return { y: "100%" };
685
+ default: return { x: "100%" };
686
+ }
687
+ }
688
+ /** Motion animate target — the resting position. */
689
+ function getSlideTarget() {
690
+ return {
691
+ x: 0,
692
+ y: 0
693
+ };
694
+ }
695
+ /** Translate offset that pushes stacked panels away from the stack edge. */
696
+ function getStackOffset(side, offset) {
697
+ if (offset === 0) return {};
698
+ switch (side) {
699
+ case "right": return { x: -offset };
700
+ case "left": return { x: offset };
701
+ case "bottom": return { y: -offset };
702
+ default: return {};
703
+ }
704
+ }
705
+ /** Opposite-side origin so stacked panels recede away from the stack edge. */
706
+ function getTransformOrigin(side) {
707
+ if (side === "right") return "left center";
708
+ if (side === "left") return "right center";
709
+ return "center top";
710
+ }
711
+ /**
712
+ * Fixed-position styles for a panel, accounting for side, width, and depth.
713
+ */
714
+ function getPanelStyles(side, config, index) {
715
+ const { width, maxWidth, zIndex } = config;
716
+ const base = {
717
+ position: "fixed",
718
+ zIndex: zIndex + 10 + index,
719
+ display: "flex",
720
+ flexDirection: "column",
721
+ willChange: "transform",
722
+ transformOrigin: getTransformOrigin(side)
723
+ };
724
+ if (side === "bottom") return {
725
+ ...base,
726
+ left: 0,
727
+ right: 0,
728
+ bottom: 0,
729
+ height: "85dvh",
730
+ maxHeight: "85dvh"
731
+ };
732
+ const sideStyles = side === "right" ? {
733
+ top: 0,
734
+ right: 0,
735
+ bottom: 0
736
+ } : {
737
+ top: 0,
738
+ left: 0,
739
+ bottom: 0
740
+ };
741
+ return {
742
+ ...base,
743
+ ...sideStyles,
744
+ width,
745
+ maxWidth
746
+ };
747
+ }
748
+ //#endregion
749
+ //#region src/icons.tsx
750
+ /** Inline SVG icons — no external dependency */
751
+ function ArrowLeftIcon() {
752
+ return /* @__PURE__ */ jsx("svg", {
753
+ "aria-hidden": "true",
754
+ fill: "none",
755
+ height: 16,
756
+ stroke: "currentColor",
757
+ strokeLinecap: "round",
758
+ strokeLinejoin: "round",
759
+ strokeWidth: 2,
760
+ viewBox: "0 0 24 24",
761
+ width: 16,
762
+ children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 19l-7-7 7-7" })
763
+ });
764
+ }
765
+ function XIcon() {
766
+ return /* @__PURE__ */ jsx("svg", {
767
+ "aria-hidden": "true",
768
+ fill: "none",
769
+ height: 16,
770
+ stroke: "currentColor",
771
+ strokeLinecap: "round",
772
+ strokeLinejoin: "round",
773
+ strokeWidth: 2,
774
+ viewBox: "0 0 24 24",
775
+ width: 16,
776
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
777
+ });
778
+ }
779
+ //#endregion
780
+ //#region src/SheetPanel/DefaultHeader.tsx
781
+ function DefaultHeader({ isNested, onBack, onClose, className }) {
782
+ return /* @__PURE__ */ jsxs("div", {
783
+ className: `flex shrink-0 items-center justify-between px-4 pt-4 pb-2 ${className ?? ""}`,
784
+ children: [/* @__PURE__ */ jsx("div", {
785
+ className: "flex items-center gap-2",
786
+ children: isNested && /* @__PURE__ */ jsx("button", {
787
+ "aria-label": "Back",
788
+ className: "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-black/5 p-0 text-inherit opacity-70 transition-opacity duration-150 hover:opacity-100",
789
+ onClick: onBack,
790
+ type: "button",
791
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
792
+ })
793
+ }), /* @__PURE__ */ jsx("button", {
794
+ "aria-label": "Close",
795
+ className: "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-black/5 p-0 text-inherit opacity-70 transition-opacity duration-150 hover:opacity-100",
796
+ onClick: onClose,
797
+ type: "button",
798
+ children: /* @__PURE__ */ jsx(XIcon, {})
799
+ })]
800
+ });
801
+ }
802
+ //#endregion
803
+ //#region src/SheetPanel/SheetPanelContent.tsx
804
+ const PanelInnerContent = memo(function PanelInnerContent({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) {
805
+ if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
806
+ return /* @__PURE__ */ jsxs(Fragment, { children: [renderHeader ? renderHeader(headerProps) : /* @__PURE__ */ jsx(DefaultHeader, {
807
+ ...headerProps,
808
+ className: headerClassName
809
+ }), shouldRender && Content && /* @__PURE__ */ jsx("div", {
810
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
811
+ "data-stacksheet-no-drag": "",
812
+ children: /* @__PURE__ */ jsx(Content, { ...data })
813
+ })] });
814
+ });
815
+ PanelInnerContent.displayName = "PanelInnerContent";
816
+ function resolvePanelLayout(layout, renderHeader) {
817
+ if (layout) return layout;
818
+ return renderHeader === false ? "composable" : "classic";
819
+ }
820
+ //#endregion
821
+ //#region src/SheetPanel/SheetPanelFocus.tsx
822
+ const LAYERED_MODAL_SELECTORS = [
823
+ "[role=\"dialog\"][data-state=\"open\"]",
824
+ "[role=\"alertdialog\"][data-state=\"open\"]",
825
+ "[data-radix-popper-content-wrapper]",
826
+ "[data-radix-focus-guard]"
827
+ ].join(", ");
828
+ function ModalFocusTrap({ enabled, active, fallbackRef, children }) {
829
+ const paused = useLayeredModalFocused(enabled && active);
830
+ if (!enabled) return children;
831
+ return /* @__PURE__ */ jsx(FocusTrap, {
832
+ active,
833
+ focusTrapOptions: {
834
+ initialFocus: false,
835
+ returnFocusOnDeactivate: true,
836
+ escapeDeactivates: false,
837
+ allowOutsideClick: true,
838
+ checkCanFocusTrap: () => new Promise((resolve) => requestAnimationFrame(() => resolve())),
839
+ fallbackFocus: () => {
840
+ if (fallbackRef.current) return fallbackRef.current;
841
+ return document.body;
842
+ }
843
+ },
844
+ paused,
845
+ children
846
+ });
847
+ }
848
+ function useLayeredModalFocused(active) {
849
+ const [layered, setLayered] = useState(false);
850
+ useEffect(() => {
851
+ if (!active) {
852
+ setLayered(false);
853
+ return;
854
+ }
855
+ const evaluate = () => {
856
+ const target = document.activeElement;
857
+ if (!target || target === document.body) {
858
+ setLayered(false);
859
+ return;
860
+ }
861
+ setLayered(target instanceof Element && target.closest(LAYERED_MODAL_SELECTORS) !== null);
862
+ };
863
+ evaluate();
864
+ const handler = () => evaluate();
865
+ document.addEventListener("focusin", handler, true);
866
+ return () => document.removeEventListener("focusin", handler, true);
867
+ }, [active]);
868
+ return layered;
869
+ }
870
+ //#endregion
871
+ //#region src/SheetPanel/SheetPanelHandles.tsx
872
+ function BottomHandle({ onDismiss }) {
873
+ return /* @__PURE__ */ jsx("button", {
874
+ "aria-label": "Dismiss",
875
+ className: "absolute inset-x-0 top-0 z-10 flex w-full cursor-grab touch-none items-center justify-center border-none bg-transparent pt-2.5 pb-2",
876
+ "data-stacksheet-handle": "",
877
+ onClick: onDismiss,
878
+ type: "button",
879
+ children: /* @__PURE__ */ jsx("div", {
880
+ "aria-hidden": "true",
881
+ className: "h-[5px] w-9 rounded-full bg-current/15"
882
+ })
883
+ });
884
+ }
885
+ function SideHandle({ side, isHovered, onDismiss }) {
886
+ const position = side === "right" ? { right: "100%" } : { left: "100%" };
887
+ return /* @__PURE__ */ jsx(motion.div, {
888
+ animate: { opacity: isHovered ? 1 : 0 },
889
+ "aria-label": "Dismiss",
890
+ className: "absolute top-0 bottom-0 flex w-6 cursor-grab touch-none items-center justify-center",
891
+ "data-stacksheet-handle": "",
892
+ onClick: onDismiss,
893
+ onKeyDown: (e) => {
894
+ if (e.key === "Enter" || e.key === " ") {
895
+ e.preventDefault();
896
+ onDismiss?.();
897
+ }
898
+ },
899
+ role: "button",
900
+ style: position,
901
+ tabIndex: 0,
902
+ transition: {
903
+ duration: isHovered ? .15 : .4,
904
+ ease: "easeOut"
905
+ },
906
+ children: /* @__PURE__ */ jsx("div", {
907
+ "aria-hidden": "true",
908
+ className: "h-8 w-1 rounded-full bg-current/20"
909
+ })
910
+ });
911
+ }
912
+ //#endregion
913
+ //#region src/SheetPanel/SheetPanel.tsx
914
+ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNested, side, config, classNames, Content, shouldRender, pop, close, swipeClose, swipePop, snapHeights, activeSnapIndex, onSnap, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring, prefersReducedMotion }) {
915
+ const panelRef = useRef(null);
916
+ const hasEnteredRef = useRef(false);
917
+ const [dragState, setDragState] = useState({
918
+ offset: 0,
919
+ isDragging: false
920
+ });
921
+ const [isHovered, setIsHovered] = useState(false);
922
+ const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
923
+ const transform = getStackTransform(depth, config.stacking);
924
+ const panelStyles = getPanelStyles(side, config, index);
925
+ useEffect(() => {
926
+ if (!isTop) hasEnteredRef.current = false;
927
+ }, [isTop]);
928
+ const handleAnimationComplete = useCallback(() => {
929
+ if (isTop && !hasEnteredRef.current) {
930
+ hasEnteredRef.current = true;
931
+ config.onOpenComplete?.();
932
+ }
933
+ }, [isTop, config]);
934
+ useDrag(panelRef, {
935
+ enabled: isTop && config.drag && config.dismissible && !prefersReducedMotion,
936
+ closeThreshold: config.closeThreshold,
937
+ velocityThreshold: config.velocityThreshold,
938
+ side,
939
+ onClose: swipeClose,
940
+ onPop: swipePop,
941
+ isNested,
942
+ snapHeights,
943
+ activeSnapIndex,
944
+ onSnap,
945
+ sequential: config.snapToSequentialPoints
946
+ }, setDragState);
947
+ const ariaLabel = item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? config.ariaLabel;
948
+ const panelId = `stacksheet-${item.id}`;
949
+ const [hasDescription, setHasDescription] = useState(false);
950
+ const registerDescription = useCallback(() => {
951
+ setHasDescription(true);
952
+ return () => setHasDescription(false);
953
+ }, []);
954
+ const panelContext = useMemo(() => ({
955
+ close,
956
+ back: pop,
957
+ isNested,
958
+ isTop,
959
+ panelId,
960
+ side,
961
+ hasDescription,
962
+ registerDescription
963
+ }), [
964
+ close,
965
+ pop,
966
+ isNested,
967
+ isTop,
968
+ panelId,
969
+ side,
970
+ hasDescription,
971
+ registerDescription
972
+ ]);
973
+ const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
974
+ const hasPanelClass = classNames.panel !== "";
975
+ const dragOffset = getDragTransform(side, dragState.offset);
976
+ const panelStyle = buildPanelStyle(panelStyles, isTop, hasPanelClass, dragState.isDragging);
977
+ const headerProps = useMemo(() => ({
978
+ isNested,
979
+ onBack: pop,
980
+ onClose: close,
981
+ side
982
+ }), [
983
+ close,
984
+ isNested,
985
+ pop,
986
+ side
987
+ ]);
988
+ const ariaProps = buildAriaProps(isTop, config.modal, isComposable, ariaLabel, panelId, hasDescription);
989
+ const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
990
+ const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
991
+ const snapYOffset = computeSnapYOffset(side, snapHeights, activeSnapIndex, measuredHeight);
992
+ const resolvedSlideFrom = resolveSlideFrom(side, slideFrom, measuredHeight);
993
+ const animateTarget = buildAnimateTarget(slideTarget, getStackOffset(side, transform.offset), dragOffset, transform, animatedRadius, transition, snapYOffset, isTop);
994
+ const initialRadius = getInitialRadius(side);
995
+ const showSideHandle = isTop && side !== "bottom";
996
+ const showBottomHandle = isTop && side === "bottom";
997
+ const panelContent = /* @__PURE__ */ jsxs(motion.div, {
998
+ animate: animateTarget,
999
+ className: classNames.panel || void 0,
1000
+ exit: {
1001
+ ...resolvedSlideFrom,
1002
+ opacity: .6,
1003
+ boxShadow: getShadow(false),
1004
+ transition: {
1005
+ type: "tween",
1006
+ duration: prefersReducedMotion ? 0 : .24,
1007
+ ease: "easeOut",
1008
+ boxShadow: VISUAL_TWEEN
1009
+ }
1010
+ },
1011
+ initial: {
1012
+ ...resolvedSlideFrom,
1013
+ opacity: .8,
1014
+ ...initialRadius,
1015
+ boxShadow: getShadow(false)
1016
+ },
1017
+ onAnimationComplete: handleAnimationComplete,
1018
+ onBlur: showSideHandle ? () => setIsHovered(false) : void 0,
1019
+ onFocus: showSideHandle ? () => setIsHovered(true) : void 0,
1020
+ onMouseEnter: showSideHandle ? () => setIsHovered(true) : void 0,
1021
+ onMouseLeave: showSideHandle ? () => setIsHovered(false) : void 0,
1022
+ ref: panelRef,
1023
+ style: panelStyle,
1024
+ tabIndex: isTop ? -1 : void 0,
1025
+ ...isTop ? {} : {
1026
+ "aria-hidden": "true",
1027
+ inert: true
1028
+ },
1029
+ ...ariaProps,
1030
+ children: [showSideHandle && /* @__PURE__ */ jsx(SideHandle, {
1031
+ isHovered,
1032
+ onDismiss: isNested ? pop : close,
1033
+ side
1034
+ }), /* @__PURE__ */ jsxs("div", {
1035
+ className: "relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[inherit]",
1036
+ children: [showBottomHandle && /* @__PURE__ */ jsx(BottomHandle, { onDismiss: isNested ? pop : close }), /* @__PURE__ */ jsx(PanelInnerContent, {
1037
+ Content,
1038
+ data: item.data,
1039
+ headerClassName: classNames.header || void 0,
1040
+ headerProps,
1041
+ isComposable,
1042
+ renderHeader,
1043
+ shouldRender
1044
+ })]
1045
+ })]
1046
+ }, item.id);
1047
+ return /* @__PURE__ */ jsx(SheetPanelContext.Provider, {
1048
+ value: panelContext,
1049
+ children: /* @__PURE__ */ jsx(ModalFocusTrap, {
1050
+ active: isTop,
1051
+ enabled: config.modal,
1052
+ fallbackRef: panelRef,
1053
+ children: panelContent
1054
+ })
1055
+ });
1056
+ });
1057
+ //#endregion
1058
+ //#region src/renderer.tsx
1059
+ /**
1060
+ * Root renderer component — manages the backdrop, scroll lock, snap points,
1061
+ * close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
1062
+ * delegates per-panel rendering to `SheetPanel`.
1063
+ *
1064
+ * Mounted inside a Portal by `StacksheetProvider`.
1065
+ */
1066
+ function SheetRenderer({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) {
1067
+ const isOpen = useStore(store, (s) => s.isOpen);
1068
+ const stack = useStore(store, (s) => s.stack);
1069
+ const rawClose = useStore(store, (s) => s.close);
1070
+ const rawPop = useStore(store, (s) => s.pop);
1071
+ const side = useResolvedSide(config);
1072
+ const prefersReducedMotion = useReducedMotion() ?? false;
1073
+ const classNames = useMemo(() => resolveClassNames(classNamesProp), [classNamesProp]);
1074
+ const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
1075
+ const snapHeights = useMemo(() => side === "bottom" && config.snapPoints.length > 0 ? resolveSnapPoints(config.snapPoints, viewportHeight) : [], [
1076
+ side,
1077
+ config.snapPoints,
1078
+ viewportHeight
1079
+ ]);
1080
+ const [internalSnapIndex, setInternalSnapIndex] = useState(snapHeights.length > 0 ? snapHeights.length - 1 : 0);
1081
+ const activeSnapIndex = config.snapPointIndex ?? internalSnapIndex;
1082
+ const handleSnap = useCallback((index) => {
1083
+ setInternalSnapIndex(index);
1084
+ config.onSnapPointChange?.(index);
1085
+ }, [config.onSnapPointChange, config]);
1086
+ useEffect(() => {
1087
+ if (isOpen && snapHeights.length > 0) setInternalSnapIndex(config.snapPointIndex ?? snapHeights.length - 1);
1088
+ }, [
1089
+ isOpen,
1090
+ snapHeights.length,
1091
+ config.snapPointIndex
1092
+ ]);
1093
+ const closeReasonRef = useRef("programmatic");
1094
+ const closeWith = useCallback((reason) => {
1095
+ closeReasonRef.current = reason;
1096
+ rawClose();
1097
+ }, [rawClose]);
1098
+ const popWith = useCallback((reason) => {
1099
+ closeReasonRef.current = reason;
1100
+ rawPop();
1101
+ }, [rawPop]);
1102
+ const close = useCallback(() => closeWith("programmatic"), [closeWith]);
1103
+ const pop = useCallback(() => popWith("programmatic"), [popWith]);
1104
+ useBodyScale(config, isOpen, prefersReducedMotion);
1105
+ const triggerRef = useRef(null);
1106
+ const wasOpenRef = useRef(false);
1107
+ useEffect(() => {
1108
+ if (isOpen && !wasOpenRef.current) triggerRef.current = document.activeElement;
1109
+ else if (!isOpen && wasOpenRef.current) {
1110
+ const el = triggerRef.current;
1111
+ if (el && el instanceof HTMLElement && el !== document.body && el.tagName !== "BODY") el.focus();
1112
+ triggerRef.current = null;
1113
+ }
1114
+ wasOpenRef.current = isOpen;
1115
+ }, [isOpen]);
1116
+ const stackLengthRef = useRef(stack.length);
1117
+ useEffect(() => {
1118
+ stackLengthRef.current = stack.length;
1119
+ }, [stack.length]);
1120
+ useEffect(() => {
1121
+ if (!(isOpen && config.closeOnEscape && config.dismissible)) return;
1122
+ function handleKeyDown(e) {
1123
+ if (e.key === "Escape") {
1124
+ e.preventDefault();
1125
+ if (stackLengthRef.current > 1) popWith("escape");
1126
+ else closeWith("escape");
1127
+ }
1128
+ }
1129
+ document.addEventListener("keydown", handleKeyDown);
1130
+ return () => document.removeEventListener("keydown", handleKeyDown);
1131
+ }, [
1132
+ isOpen,
1133
+ config.closeOnEscape,
1134
+ config.dismissible,
1135
+ popWith,
1136
+ closeWith
1137
+ ]);
1138
+ useEffect(() => {
1139
+ if (!(isOpen && config.dismissible) || typeof globalThis.CloseWatcher === "undefined") return;
1140
+ const watcher = new globalThis.CloseWatcher();
1141
+ watcher.onclose = () => {
1142
+ if (stackLengthRef.current > 1) popWith("escape");
1143
+ else closeWith("escape");
1144
+ };
1145
+ return () => watcher.destroy();
1146
+ }, [
1147
+ isOpen,
1148
+ config.dismissible,
1149
+ popWith,
1150
+ closeWith
1151
+ ]);
1152
+ const slideFrom = useMemo(() => getSlideFrom(side), [side]);
1153
+ const slideTarget = useMemo(() => getSlideTarget(), []);
1154
+ const spring = useMemo(() => prefersReducedMotion ? {
1155
+ type: "tween",
1156
+ duration: 0
1157
+ } : {
1158
+ type: "spring",
1159
+ damping: config.spring.damping,
1160
+ stiffness: config.spring.stiffness,
1161
+ mass: config.spring.mass
1162
+ }, [
1163
+ prefersReducedMotion,
1164
+ config.spring.damping,
1165
+ config.spring.stiffness,
1166
+ config.spring.mass
1167
+ ]);
1168
+ const stackSpring = spring;
1169
+ const isModal = config.modal;
1170
+ const showOverlay = isModal && config.showOverlay;
1171
+ const hasBackdropClass = classNames.backdrop !== "";
1172
+ const backdropStyle = {
1173
+ zIndex: config.zIndex,
1174
+ willChange: "opacity",
1175
+ cursor: config.closeOnBackdrop && config.dismissible ? "pointer" : void 0,
1176
+ ...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
1177
+ };
1178
+ const handleExitComplete = useCallback(() => {
1179
+ if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
1180
+ }, [stack.length, config]);
1181
+ const handleBackdropExitComplete = useCallback(() => {
1182
+ requestAnimationFrame(() => {
1183
+ document.body.offsetHeight;
1184
+ });
1185
+ }, []);
1186
+ const swipeClose = useCallback(() => closeWith("swipe"), [closeWith]);
1187
+ const swipePop = useCallback(() => popWith("swipe"), [popWith]);
1188
+ const shouldLockScroll = isOpen && isModal && config.lockScroll;
1189
+ return /* @__PURE__ */ jsxs(Fragment, { children: [showOverlay && /* @__PURE__ */ jsx(AnimatePresence, {
1190
+ onExitComplete: handleBackdropExitComplete,
1191
+ children: isOpen && /* @__PURE__ */ jsx(motion.div, {
1192
+ animate: { opacity: 1 },
1193
+ className: `fixed inset-0 ${classNames.backdrop || ""}`,
1194
+ exit: { opacity: 0 },
1195
+ initial: { opacity: 0 },
1196
+ onClick: config.closeOnBackdrop && config.dismissible ? () => closeWith("backdrop") : void 0,
1197
+ style: backdropStyle,
1198
+ transition: spring
1199
+ }, "stacksheet-backdrop")
1200
+ }), /* @__PURE__ */ jsx(RemoveScroll, {
1201
+ enabled: shouldLockScroll,
1202
+ forwardProps: true,
1203
+ children: /* @__PURE__ */ jsx("div", {
1204
+ className: "pointer-events-none fixed inset-0 overflow-hidden",
1205
+ style: { zIndex: config.zIndex + 1 },
1206
+ children: /* @__PURE__ */ jsx(AnimatePresence, {
1207
+ onExitComplete: handleExitComplete,
1208
+ children: stack.map((item, index) => {
1209
+ const depth = stack.length - 1 - index;
1210
+ const isTop = depth === 0;
1211
+ const isNested = index > 0;
1212
+ const shouldRender = depth <= config.stacking.renderThreshold;
1213
+ return /* @__PURE__ */ jsx(SheetPanel, {
1214
+ activeSnapIndex,
1215
+ Content: componentMap.get(item.type) ?? sheets[item.type],
1216
+ classNames,
1217
+ close,
1218
+ config,
1219
+ depth,
1220
+ index,
1221
+ isNested,
1222
+ isTop,
1223
+ item,
1224
+ layout,
1225
+ onSnap: handleSnap,
1226
+ pop,
1227
+ prefersReducedMotion,
1228
+ renderHeader,
1229
+ shouldRender,
1230
+ side,
1231
+ slideFrom,
1232
+ slideTarget,
1233
+ snapHeights,
1234
+ spring,
1235
+ stackSpring,
1236
+ swipeClose,
1237
+ swipePop
1238
+ }, item.id);
1239
+ })
1240
+ })
1241
+ })
1242
+ })] });
1243
+ }
1244
+ //#endregion
1245
+ //#region src/Store/StoreArgs.ts
1246
+ function resolvePresentationOptions(value) {
1247
+ if (!(value && typeof value === "object")) return;
1248
+ const candidate = value;
1249
+ if (candidate.ariaLabel !== void 0 && typeof candidate.ariaLabel !== "string") return;
1250
+ return candidate;
1251
+ }
1252
+ /**
1253
+ * Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
1254
+ * When a new component reference has the same displayName/name as an existing one,
1255
+ * it's almost always an inline arrow being re-created every render.
1256
+ */
1257
+ function warnInlineComponent(component, componentRegistry, warnedNames) {
1258
+ if (typeof process === "undefined" || process?.env?.NODE_ENV === "production") return;
1259
+ const name = component.displayName || component.name;
1260
+ if (!name) return;
1261
+ if (warnedNames.has(name)) return;
1262
+ for (const [existing, key] of componentRegistry) if ((existing.displayName || existing.name) === name) {
1263
+ warnedNames.add(name);
1264
+ console.warn(`[stacksheet] A new component reference with name "${name}" was registered (key: ${key}), but a different reference with the same name already exists. This usually means you're passing an inline arrow function (e.g. open(() => <MySheet />)). Define the component outside of render to avoid memory leaks and broken navigate() same-type detection.`);
1265
+ return;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * If `first` is a function (component), register it and return { type, id, data }.
1270
+ * Otherwise, pass through the string-based (type, id, data) args unchanged.
1271
+ */
1272
+ function resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) {
1273
+ if (typeof first === "function") {
1274
+ const component = first;
1275
+ let typeKey = componentRegistry.get(component);
1276
+ if (!typeKey) {
1277
+ warnInlineComponent(component, componentRegistry, warnedNames);
1278
+ typeKey = getNextKey();
1279
+ componentRegistry.set(component, typeKey);
1280
+ componentMap.set(typeKey, component);
1281
+ }
1282
+ if (typeof second === "string") return {
1283
+ ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1284
+ type: typeKey,
1285
+ id: second,
1286
+ data: third ?? {}
1287
+ };
1288
+ return {
1289
+ ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
1290
+ type: typeKey,
1291
+ id: crypto.randomUUID(),
1292
+ data: second ?? {}
1293
+ };
1294
+ }
1295
+ return {
1296
+ ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1297
+ type: first,
1298
+ id: second,
1299
+ data: third ?? {}
1300
+ };
1301
+ }
1302
+ //#endregion
1303
+ //#region src/Store/createSheetStore.ts
1304
+ /**
1305
+ * Create an isolated Zustand store for a sheet stack instance.
1306
+ *
1307
+ * Returns a store bundle containing the Zustand store plus two maps
1308
+ * that track ad-hoc (component-direct) registrations:
1309
+ * - `componentRegistry` — maps `ComponentType` → generated type key (dedup)
1310
+ * - `componentMap` — maps generated type key → `ComponentType` (renderer lookup)
1311
+ *
1312
+ * The ad-hoc counter is scoped per instance to prevent identity leaks across
1313
+ * multiple `createStacksheet()` calls or test runs.
1314
+ */
1315
+ function createSheetStore(config) {
1316
+ const componentRegistry = /* @__PURE__ */ new Map();
1317
+ const componentMap = /* @__PURE__ */ new Map();
1318
+ let adhocCounter = 0;
1319
+ function getNextKey() {
1320
+ return `__adhoc_${adhocCounter++}`;
1321
+ }
1322
+ const warnedNames = /* @__PURE__ */ new Set();
1323
+ function resolve(first, second, third, fourth) {
1324
+ return resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth);
1325
+ }
1326
+ /** Remove registry entries for type keys no longer in the stack */
1327
+ function pruneRegistry(remainingStack) {
1328
+ const usedTypes = new Set(remainingStack.map((item) => item.type));
1329
+ for (const [component, typeKey] of componentRegistry) if (!usedTypes.has(typeKey)) {
1330
+ componentRegistry.delete(component);
1331
+ componentMap.delete(typeKey);
1332
+ }
1333
+ }
1334
+ return {
1335
+ store: createStore()((set, get) => {
1336
+ function _openResolved({ type, id, data, ariaLabel }) {
1337
+ set({
1338
+ stack: [{
1339
+ id,
1340
+ type,
1341
+ data,
1342
+ ariaLabel
1343
+ }],
1344
+ isOpen: true
1345
+ });
1346
+ }
1347
+ function _pushResolved({ type, id, data, ariaLabel }) {
1348
+ set((state) => {
1349
+ const item = {
1350
+ id,
1351
+ type,
1352
+ data,
1353
+ ariaLabel
1354
+ };
1355
+ if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
1356
+ stack: [...state.stack.slice(0, -1), item],
1357
+ isOpen: true
1358
+ };
1359
+ return {
1360
+ stack: [...state.stack, item],
1361
+ isOpen: true
1362
+ };
1363
+ });
1364
+ }
1365
+ function _replaceResolved({ type, id, data, ariaLabel }) {
1366
+ set((state) => {
1367
+ const item = {
1368
+ id,
1369
+ type,
1370
+ data,
1371
+ ariaLabel
1372
+ };
1373
+ if (state.stack.length === 0) return {
1374
+ stack: [item],
1375
+ isOpen: true
1376
+ };
1377
+ return {
1378
+ stack: [...state.stack.slice(0, -1), item],
1379
+ isOpen: true
1380
+ };
1381
+ });
1382
+ }
1383
+ return {
1384
+ stack: [],
1385
+ isOpen: false,
1386
+ open(first, second, third, fourth) {
1387
+ _openResolved(resolve(first, second, third, fourth));
1388
+ },
1389
+ push(first, second, third, fourth) {
1390
+ _pushResolved(resolve(first, second, third, fourth));
1391
+ },
1392
+ replace(first, second, third, fourth) {
1393
+ _replaceResolved(resolve(first, second, third, fourth));
1394
+ },
1395
+ swap(first, second, third) {
1396
+ let type;
1397
+ let data;
1398
+ const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
1399
+ if (typeof first === "function") {
1400
+ const component = first;
1401
+ let typeKey = componentRegistry.get(component);
1402
+ if (!typeKey) {
1403
+ warnInlineComponent(component, componentRegistry, warnedNames);
1404
+ typeKey = getNextKey();
1405
+ componentRegistry.set(component, typeKey);
1406
+ componentMap.set(typeKey, component);
1407
+ }
1408
+ type = typeKey;
1409
+ data = second ?? {};
1410
+ } else {
1411
+ type = first;
1412
+ data = second ?? {};
1413
+ }
1414
+ set((state) => {
1415
+ const top = state.stack.at(-1);
1416
+ if (!top) return state;
1417
+ const newStack = [...state.stack];
1418
+ newStack[newStack.length - 1] = {
1419
+ id: top.id,
1420
+ type,
1421
+ data,
1422
+ ariaLabel: ariaLabel ?? top.ariaLabel
1423
+ };
1424
+ return { stack: newStack };
1425
+ });
1426
+ },
1427
+ navigate(first, second, third, fourth) {
1428
+ const resolved = resolve(first, second, third, fourth);
1429
+ const { stack } = get();
1430
+ const top = stack.at(-1);
1431
+ if (stack.length === 0) {
1432
+ _openResolved(resolved);
1433
+ return;
1434
+ }
1435
+ let isSameType = top?.type === resolved.type;
1436
+ if (!isSameType && typeof first === "function") isSameType = componentMap.get(top?.type ?? "") === first;
1437
+ if (isSameType) {
1438
+ _replaceResolved(resolved);
1439
+ return;
1440
+ }
1441
+ _pushResolved(resolved);
1442
+ },
1443
+ setData(first, second, third) {
1444
+ const { id, data } = resolve(first, second, third);
1445
+ set((state) => {
1446
+ const idx = state.stack.findIndex((item) => item.id === id);
1447
+ if (idx === -1) return state;
1448
+ const updated = [...state.stack];
1449
+ updated[idx] = {
1450
+ ...updated[idx],
1451
+ data
1452
+ };
1453
+ return { stack: updated };
1454
+ });
1455
+ },
1456
+ remove(id) {
1457
+ set((state) => {
1458
+ const next = state.stack.filter((item) => item.id !== id);
1459
+ if (next.length === state.stack.length) return state;
1460
+ pruneRegistry(next);
1461
+ if (next.length === 0) return {
1462
+ stack: [],
1463
+ isOpen: false
1464
+ };
1465
+ return { stack: next };
1466
+ });
1467
+ },
1468
+ pop() {
1469
+ set((state) => {
1470
+ if (state.stack.length <= 1) {
1471
+ pruneRegistry([]);
1472
+ return {
1473
+ stack: [],
1474
+ isOpen: false
1475
+ };
1476
+ }
1477
+ const next = state.stack.slice(0, -1);
1478
+ pruneRegistry(next);
1479
+ return {
1480
+ stack: next,
1481
+ isOpen: true
1482
+ };
1483
+ });
1484
+ },
1485
+ close() {
1486
+ pruneRegistry([]);
1487
+ set({
1488
+ stack: [],
1489
+ isOpen: false
1490
+ });
1491
+ }
1492
+ };
1493
+ }),
1494
+ componentRegistry,
1495
+ componentMap
1496
+ };
1497
+ }
1498
+ //#endregion
1499
+ //#region src/create.tsx
1500
+ /**
1501
+ * Create an isolated sheet stack instance with typed store, hooks, and provider.
1502
+ *
1503
+ * Works with both `interface` and `type` definitions:
1504
+ *
1505
+ * ```ts
1506
+ * // Using an interface
1507
+ * interface SheetDataMap {
1508
+ * "bucket-create": { onCreated?: (b: Bucket) => void };
1509
+ * "bucket-edit": { bucket: Bucket };
1510
+ * }
1511
+ *
1512
+ * // Using a type alias
1513
+ * type SheetDataMap = {
1514
+ * "bucket-create": { onCreated?: (b: Bucket) => void };
1515
+ * "bucket-edit": { bucket: Bucket };
1516
+ * };
1517
+ *
1518
+ * const { StacksheetProvider, useSheet, useStacksheetState } =
1519
+ * createStacksheet<SheetDataMap>();
1520
+ * ```
1521
+ *
1522
+ * Sheet content components receive their data as **spread props**:
1523
+ * ```ts
1524
+ * // Data map defines: "bucket-edit": { bucket: Bucket }
1525
+ * // Component receives: ({ bucket }: { bucket: Bucket }) => JSX.Element
1526
+ * ```
1527
+ *
1528
+ * Use `useSheetPanel()` inside content components to access `close()` and `back()`.
1529
+ */
1530
+ function createStacksheet(config) {
1531
+ const resolved = resolveConfig(config);
1532
+ const { store, componentMap } = createSheetStore(resolved);
1533
+ const StoreContext = createContext(null);
1534
+ function useStoreContext() {
1535
+ const ctx = useContext(StoreContext);
1536
+ if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
1537
+ return ctx;
1538
+ }
1539
+ const EMPTY_SHEETS = {};
1540
+ function StacksheetProvider({ sheets = EMPTY_SHEETS, children, classNames, layout, renderHeader }) {
1541
+ const value = useMemo(() => ({
1542
+ store,
1543
+ config: resolved
1544
+ }), []);
1545
+ return /* @__PURE__ */ jsxs(StoreContext.Provider, {
1546
+ value,
1547
+ children: [children, /* @__PURE__ */ jsx(Portal, {
1548
+ asChild: false,
1549
+ children: /* @__PURE__ */ jsx(SheetRenderer, {
1550
+ classNames,
1551
+ componentMap,
1552
+ config: resolved,
1553
+ layout,
1554
+ renderHeader,
1555
+ sheets,
1556
+ store
1557
+ })
1558
+ })]
1559
+ });
1560
+ }
1561
+ function useSheet() {
1562
+ const { store: s } = useStoreContext();
1563
+ return useMemo(() => {
1564
+ const state = s.getState();
1565
+ return {
1566
+ open: state.open,
1567
+ push: state.push,
1568
+ replace: state.replace,
1569
+ swap: state.swap,
1570
+ navigate: state.navigate,
1571
+ setData: state.setData,
1572
+ remove: state.remove,
1573
+ pop: state.pop,
1574
+ close: state.close
1575
+ };
1576
+ }, [s]);
1577
+ }
1578
+ function useStacksheetState() {
1579
+ const { store: s } = useStoreContext();
1580
+ return useStore(s, useShallow((state) => ({
1581
+ stack: state.stack,
1582
+ isOpen: state.isOpen
1583
+ })));
1584
+ }
1585
+ return {
1586
+ StacksheetProvider,
1587
+ useSheet,
1588
+ useStacksheetState,
1589
+ store
1590
+ };
1591
+ }
1592
+ //#endregion
1593
+ //#region src/SheetParts/SheetBack.tsx
1594
+ function SheetBack({ asChild, className, style, children }) {
1595
+ const { back, isNested } = useSheetPanel();
1596
+ if (!isNested) return null;
1597
+ return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1598
+ "aria-label": children ? void 0 : "Back",
1599
+ className: joinClassNames(asChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1600
+ onClick: back,
1601
+ style,
1602
+ type: asChild ? void 0 : "button",
1603
+ children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
1604
+ });
1605
+ }
1606
+ //#endregion
1607
+ //#region src/SheetParts/SheetBody.tsx
1608
+ function SheetBody({ asChild, className, style, children }) {
1609
+ if (asChild) return /* @__PURE__ */ jsx(Slot, {
1610
+ className: joinClassNames("relative min-h-0 flex-1", className),
1611
+ "data-stacksheet-no-drag": "",
1612
+ style,
1613
+ children
1614
+ });
1615
+ return /* @__PURE__ */ jsxs(Root, {
1616
+ className: joinClassNames("relative flex min-h-0 flex-1 flex-col overflow-hidden", className),
1617
+ "data-stacksheet-no-drag": "",
1618
+ style,
1619
+ children: [/* @__PURE__ */ jsx(Viewport, {
1620
+ className: "min-h-0 w-full flex-1 overscroll-contain",
1621
+ children
1622
+ }), /* @__PURE__ */ jsx(Scrollbar, {
1623
+ className: "flex w-2 touch-none select-none p-0.5",
1624
+ orientation: "vertical",
1625
+ children: /* @__PURE__ */ jsx(Thumb, { className: "relative flex-1 rounded bg-current/15" })
1626
+ })]
1627
+ });
1628
+ }
1629
+ //#endregion
1630
+ //#region src/SheetParts/SheetClose.tsx
1631
+ function SheetClose({ asChild, className, style, children }) {
1632
+ const { close } = useSheetPanel();
1633
+ return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1634
+ "aria-label": children ? void 0 : "Close",
1635
+ className: joinClassNames(asChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1636
+ onClick: close,
1637
+ style,
1638
+ type: asChild ? void 0 : "button",
1639
+ children: children ?? /* @__PURE__ */ jsx(XIcon, {})
1640
+ });
1641
+ }
1642
+ //#endregion
1643
+ //#region src/SheetParts/SheetDescription.tsx
1644
+ function SheetDescription({ asChild, className, style, children }) {
1645
+ const { panelId, registerDescription } = useSheetPanel();
1646
+ useEffect(() => registerDescription(), [registerDescription]);
1647
+ return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
1648
+ className,
1649
+ id: `${panelId}-desc`,
1650
+ style,
1651
+ children
1652
+ });
1653
+ }
1654
+ //#endregion
1655
+ //#region src/SheetParts/SheetFooter.tsx
1656
+ function SheetFooter({ asChild, className, style, children }) {
1657
+ return /* @__PURE__ */ jsx(asChild ? Slot : "footer", {
1658
+ className: joinClassNames(asChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
1659
+ style,
1660
+ children
1661
+ });
1662
+ }
1663
+ //#endregion
1664
+ //#region src/SheetParts/SheetHandle.tsx
1665
+ function SheetHandle({ asChild, className, style, children }) {
1666
+ const { close, back, isNested } = useSheetPanel();
1667
+ const dismiss = isNested ? back : close;
1668
+ return /* @__PURE__ */ jsx(asChild ? Slot : "div", {
1669
+ "aria-label": "Dismiss",
1670
+ className: joinClassNames(asChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center pt-4 pb-1", className),
1671
+ "data-stacksheet-handle": "",
1672
+ onClick: dismiss,
1673
+ onKeyDown: (e) => {
1674
+ if (e.key === "Enter" || e.key === " ") {
1675
+ e.preventDefault();
1676
+ dismiss();
1677
+ }
1678
+ },
1679
+ role: "button",
1680
+ style,
1681
+ tabIndex: 0,
1682
+ children: children ?? /* @__PURE__ */ jsx("div", {
1683
+ "aria-hidden": "true",
1684
+ className: "h-1 w-9 rounded-sm bg-current/25"
1685
+ })
1686
+ });
1687
+ }
1688
+ //#endregion
1689
+ //#region src/SheetParts/SheetHeader.tsx
1690
+ function SheetHeader({ asChild, className, style, children }) {
1691
+ return /* @__PURE__ */ jsx(asChild ? Slot : "header", {
1692
+ className: joinClassNames(asChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
1693
+ style,
1694
+ children
1695
+ });
1696
+ }
1697
+ //#endregion
1698
+ //#region src/SheetParts/SheetTitle.tsx
1699
+ function SheetTitle({ asChild, className, style, children }) {
1700
+ const { panelId } = useSheetPanel();
1701
+ return /* @__PURE__ */ jsx(asChild ? Slot : "h2", {
1702
+ className: joinClassNames(asChild ? void 0 : "font-semibold text-sm", className),
1703
+ id: `${panelId}-title`,
1704
+ style,
1705
+ children
1706
+ });
1707
+ }
1708
+ //#endregion
1709
+ //#region src/SheetParts/SheetParts.ts
1710
+ /**
1711
+ * Composable Sheet Parts for building custom Sheet layouts.
1712
+ *
1713
+ * Use with `layout="composable"` on the provider to opt into composable mode:
1714
+ * no auto header or scroll wrapper, full control over the Sheet structure.
1715
+ *
1716
+ * `Sheet.Title` and `Sheet.Description` are linked to the Panel's
1717
+ * `aria-labelledby` and `aria-describedby` via matching IDs.
1718
+ */
1719
+ const Sheet = {
1720
+ Back: SheetBack,
1721
+ Body: SheetBody,
1722
+ Close: SheetClose,
1723
+ Description: SheetDescription,
1724
+ Footer: SheetFooter,
1725
+ Handle: SheetHandle,
1726
+ Header: SheetHeader,
1727
+ Title: SheetTitle
1728
+ };
1729
+ //#endregion
1730
+ export { Sheet, createStacksheet, springs, useIsMobile, useResolvedSide, useSheetPanel };
1731
+
1732
+ //# sourceMappingURL=index.mjs.map