@patternmode/stacksheet 1.3.6 → 1.3.8

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 (56) hide show
  1. package/dist/Drag/drag-geometry.d.ts.map +1 -1
  2. package/dist/Drag/drag-velocity.d.ts +24 -0
  3. package/dist/Drag/drag-velocity.d.ts.map +1 -0
  4. package/dist/Drag/use-drag.d.ts.map +1 -1
  5. package/dist/SheetPanel/default-header.d.ts +1 -1
  6. package/dist/SheetPanel/default-header.d.ts.map +1 -1
  7. package/dist/SheetPanel/sheet-panel-content.d.ts +1 -1
  8. package/dist/SheetPanel/sheet-panel-content.d.ts.map +1 -1
  9. package/dist/SheetPanel/sheet-panel-focus.d.ts +1 -1
  10. package/dist/SheetPanel/sheet-panel-focus.d.ts.map +1 -1
  11. package/dist/SheetPanel/sheet-panel-handles.d.ts +2 -2
  12. package/dist/SheetPanel/sheet-panel-handles.d.ts.map +1 -1
  13. package/dist/SheetPanel/sheet-panel-layout.d.ts.map +1 -1
  14. package/dist/SheetPanel/sheet-panel.d.ts +1 -1
  15. package/dist/SheetPanel/sheet-panel.d.ts.map +1 -1
  16. package/dist/SheetParts/sheet-back.d.ts +1 -1
  17. package/dist/SheetParts/sheet-back.d.ts.map +1 -1
  18. package/dist/SheetParts/sheet-body.d.ts +1 -1
  19. package/dist/SheetParts/sheet-body.d.ts.map +1 -1
  20. package/dist/SheetParts/sheet-close.d.ts +1 -1
  21. package/dist/SheetParts/sheet-close.d.ts.map +1 -1
  22. package/dist/SheetParts/sheet-description.d.ts +1 -1
  23. package/dist/SheetParts/sheet-description.d.ts.map +1 -1
  24. package/dist/SheetParts/sheet-footer.d.ts +1 -1
  25. package/dist/SheetParts/sheet-footer.d.ts.map +1 -1
  26. package/dist/SheetParts/sheet-handle.d.ts +1 -1
  27. package/dist/SheetParts/sheet-handle.d.ts.map +1 -1
  28. package/dist/SheetParts/sheet-header.d.ts +1 -1
  29. package/dist/SheetParts/sheet-header.d.ts.map +1 -1
  30. package/dist/SheetParts/sheet-parts.d.ts +8 -8
  31. package/dist/SheetParts/sheet-title.d.ts +1 -1
  32. package/dist/SheetParts/sheet-title.d.ts.map +1 -1
  33. package/dist/Store/create-sheet-store.d.ts.map +1 -1
  34. package/dist/Store/store-args.d.ts +17 -0
  35. package/dist/Store/store-args.d.ts.map +1 -1
  36. package/dist/create.d.ts.map +1 -1
  37. package/dist/icons.d.ts +2 -2
  38. package/dist/icons.d.ts.map +1 -1
  39. package/dist/index.mjs +710 -436
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/media.d.ts.map +1 -1
  42. package/dist/panel-context.d.ts +4 -0
  43. package/dist/panel-context.d.ts.map +1 -1
  44. package/dist/renderer-effects.d.ts.map +1 -1
  45. package/dist/renderer-helpers.d.ts +9 -1
  46. package/dist/renderer-helpers.d.ts.map +1 -1
  47. package/dist/renderer.d.ts +2 -2
  48. package/dist/renderer.d.ts.map +1 -1
  49. package/dist/snap-points.d.ts.map +1 -1
  50. package/dist/springs.d.ts +11 -17
  51. package/dist/springs.d.ts.map +1 -1
  52. package/dist/stacking.d.ts +2 -2
  53. package/dist/stacking.d.ts.map +1 -1
  54. package/dist/types.d.ts +31 -33
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +13 -12
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { Portal } from "@radix-ui/react-portal";
3
- import { createContext, use, useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, use, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { createStore, useStore } from "zustand";
5
5
  import { useShallow } from "zustand/react/shallow";
6
+ import { springs as springs$1 } from "@howells/motion";
6
7
  import { AnimatePresence, LazyMotion, domMax, m, useReducedMotion } from "motion/react";
7
8
  import { RemoveScroll } from "react-remove-scroll";
8
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -11,29 +12,28 @@ import { joinClassNames } from "@patternmode/system";
11
12
  import { Slot } from "@radix-ui/react-slot";
12
13
  import { Root, Scrollbar, Thumb, Viewport } from "@radix-ui/react-scroll-area";
13
14
  //#region src/springs.ts
15
+ /** Strips the shared token's `type: "spring"` down to Stacksheet's numeric SpringConfig. */
16
+ const toSpringConfig = (spring) => ({
17
+ damping: spring.damping,
18
+ mass: spring.mass,
19
+ stiffness: spring.stiffness
20
+ });
14
21
  /**
15
22
  * Spring presets inspired by iOS animation feel.
16
23
  *
17
- * - `subtle` — Barely noticeable bounce, professional.
18
- * - `snappy` — Quick, responsive for interactions.
19
- * - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
24
+ * - `subtle` — Barely noticeable bounce, professional. (shared @howells/motion token)
25
+ * - `snappy` — Quick, responsive for interactions. (shared @howells/motion token)
26
+ * - `stiff` — Very quick, controlled. Panels, drawers. **(default)** Intentional
27
+ * fork: damping 40 (tighter than the shared `stiff`) for drawer control.
20
28
  */
21
29
  const springs = {
22
- snappy: {
23
- damping: 28,
24
- mass: .8,
25
- stiffness: 400
26
- },
30
+ snappy: toSpringConfig(springs$1.snappy),
27
31
  stiff: {
28
32
  damping: 40,
29
33
  mass: 1,
30
34
  stiffness: 400
31
35
  },
32
- subtle: {
33
- damping: 30,
34
- mass: 1,
35
- stiffness: 300
36
- }
36
+ subtle: toSpringConfig(springs$1.subtle)
37
37
  };
38
38
  //#endregion
39
39
  //#region src/config.ts
@@ -114,9 +114,13 @@ const useIsMobile = (breakpoint) => {
114
114
  useEffect(() => {
115
115
  const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
116
116
  setIsMobile(mql.matches);
117
- const handler = (e) => setIsMobile(e.matches);
117
+ const handler = (e) => {
118
+ setIsMobile(e.matches);
119
+ };
118
120
  mql.addEventListener("change", handler);
119
- return () => mql.removeEventListener("change", handler);
121
+ return () => {
122
+ mql.removeEventListener("change", handler);
123
+ };
120
124
  }, [breakpoint]);
121
125
  return isMobile;
122
126
  };
@@ -130,13 +134,17 @@ const usePanelHeight = (panelRef, hasSnapPoints) => {
130
134
  const [height, setHeight] = useState(0);
131
135
  useEffect(() => {
132
136
  const el = panelRef.current;
133
- if (!(el && hasSnapPoints)) return;
134
- setHeight(el.offsetHeight);
135
- const observer = new ResizeObserver(([entry]) => {
136
- if (entry) setHeight(entry.contentRect.height);
137
- });
138
- observer.observe(el);
139
- return () => observer.disconnect();
137
+ let observer;
138
+ if (el !== null && hasSnapPoints) {
139
+ setHeight(el.offsetHeight);
140
+ observer = new ResizeObserver(([entry]) => {
141
+ if (entry) setHeight(entry.contentRect.height);
142
+ });
143
+ observer.observe(el);
144
+ }
145
+ return () => {
146
+ observer?.disconnect();
147
+ };
140
148
  }, [panelRef, hasSnapPoints]);
141
149
  return height;
142
150
  };
@@ -144,40 +152,68 @@ const getViewportHeight = () => typeof window === "undefined" ? 0 : window.visua
144
152
  const useViewportHeight = (active) => {
145
153
  const [height, setHeight] = useState(() => typeof window === "undefined" ? void 0 : getViewportHeight());
146
154
  useEffect(() => {
147
- if (typeof window === "undefined") return;
148
- const update = () => setHeight(getViewportHeight());
149
- window.addEventListener("resize", update);
150
- window.visualViewport?.addEventListener("resize", update);
155
+ const update = () => {
156
+ setHeight(getViewportHeight());
157
+ };
158
+ const canListen = typeof window !== "undefined";
159
+ if (canListen) {
160
+ window.addEventListener("resize", update);
161
+ window.visualViewport?.addEventListener("resize", update);
162
+ }
151
163
  return () => {
152
- window.removeEventListener("resize", update);
153
- window.visualViewport?.removeEventListener("resize", update);
164
+ if (canListen) {
165
+ window.removeEventListener("resize", update);
166
+ window.visualViewport?.removeEventListener("resize", update);
167
+ }
154
168
  };
155
169
  }, []);
156
170
  return active ? height ?? 0 : 0;
157
171
  };
172
+ const BODY_SCALE_TRANSITION = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
173
+ /** Fallback delay (transition duration + margin) if `transitionend` never fires. */
174
+ const BODY_SCALE_RESET_FALLBACK_MS = 600;
175
+ /** Cancels a pending un-scale reset when the sheet reopens mid-animation. */
176
+ let cancelPendingBodyScaleReset;
158
177
  const useBodyScale = (config, isOpen, prefersReducedMotion) => {
159
178
  useEffect(() => {
160
- if (!config.shouldScaleBackground || prefersReducedMotion) return;
161
179
  const wrapper = document.querySelector("[data-stacksheet-wrapper]");
162
- if (!(wrapper && wrapper instanceof HTMLElement)) return;
163
- if (isOpen) {
164
- const scale = config.scaleBackgroundAmount;
165
- wrapper.style.transition = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
166
- wrapper.style.transform = `scale(${scale})`;
167
- wrapper.style.borderRadius = "8px";
168
- wrapper.style.overflow = "hidden";
169
- wrapper.style.transformOrigin = "center top";
170
- return;
180
+ const scalable = config.shouldScaleBackground && !prefersReducedMotion && wrapper instanceof HTMLElement && isOpen ? wrapper : null;
181
+ if (scalable !== null) {
182
+ cancelPendingBodyScaleReset?.();
183
+ scalable.style.transition = BODY_SCALE_TRANSITION;
184
+ scalable.style.transform = `scale(${config.scaleBackgroundAmount})`;
185
+ scalable.style.borderRadius = "8px";
186
+ scalable.style.overflow = "hidden";
187
+ scalable.style.transformOrigin = "center top";
171
188
  }
172
- wrapper.style.transform = "";
173
- wrapper.style.borderRadius = "";
174
- const handleEnd = () => {
175
- wrapper.style.transition = "";
176
- wrapper.style.overflow = "";
177
- wrapper.style.transformOrigin = "";
189
+ return () => {
190
+ if (scalable === null) return;
191
+ scalable.style.transition = BODY_SCALE_TRANSITION;
192
+ scalable.style.transform = "";
193
+ scalable.style.borderRadius = "";
194
+ const controller = new AbortController();
195
+ const { signal } = controller;
196
+ const finish = () => {
197
+ controller.abort();
198
+ cancelPendingBodyScaleReset = void 0;
199
+ scalable.style.transition = "";
200
+ scalable.style.overflow = "";
201
+ scalable.style.transformOrigin = "";
202
+ };
203
+ scalable.addEventListener("transitionend", (event) => {
204
+ if (event.target === scalable && event.propertyName === "transform") finish();
205
+ }, { signal });
206
+ const timeoutId = setTimeout(() => {
207
+ if (!signal.aborted) finish();
208
+ }, BODY_SCALE_RESET_FALLBACK_MS);
209
+ signal.addEventListener("abort", () => {
210
+ clearTimeout(timeoutId);
211
+ });
212
+ cancelPendingBodyScaleReset = () => {
213
+ controller.abort();
214
+ cancelPendingBodyScaleReset = void 0;
215
+ };
178
216
  };
179
- wrapper.addEventListener("transitionend", handleEnd, { once: true });
180
- return () => wrapper.removeEventListener("transitionend", handleEnd);
181
217
  }, [
182
218
  isOpen,
183
219
  config.shouldScaleBackground,
@@ -187,7 +223,14 @@ const useBodyScale = (config, isOpen, prefersReducedMotion) => {
187
223
  };
188
224
  //#endregion
189
225
  //#region src/snap-points.ts
190
- const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
226
+ const SNAP_POINT_RE = /^(?<value>\d+(?:\.\d+)?)(?<unit>px|rem|em|vh|%)$/u;
227
+ const getRootFontSize = () => typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
228
+ const resolveUnitPx = (value, unit, viewportHeight) => {
229
+ if (unit === "px") return value;
230
+ if (unit === "rem" || unit === "em") return value * getRootFontSize();
231
+ if (unit === "vh" || unit === "%") return value / 100 * viewportHeight;
232
+ return 0;
233
+ };
191
234
  /**
192
235
  * Resolve a snap point value to pixels given the viewport height.
193
236
  * - number 0-1: fraction of viewport (0.5 → 50% of vh)
@@ -197,18 +240,11 @@ const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
197
240
  const resolveSnapPointPx = (point, viewportHeight) => {
198
241
  if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
199
242
  if (typeof point === "string") {
200
- const match = point.match(SNAP_POINT_RE);
201
- if (!match?.[1]) return 0;
202
- const [, rawValue, unit] = match;
203
- const value = Number.parseFloat(rawValue);
204
- switch (unit) {
205
- case "px": return value;
206
- case "rem":
207
- case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
208
- case "vh":
209
- case "%": return value / 100 * viewportHeight;
210
- default: return 0;
211
- }
243
+ const match = SNAP_POINT_RE.exec(point);
244
+ const rawValue = match?.groups?.value;
245
+ const unit = match?.groups?.unit;
246
+ if (rawValue === void 0 || unit === void 0) return 0;
247
+ return resolveUnitPx(Number.parseFloat(rawValue), unit, viewportHeight);
212
248
  }
213
249
  return 0;
214
250
  };
@@ -294,12 +330,13 @@ const resolveClassNames = (cn) => {
294
330
  panel: cn.panel ?? ""
295
331
  };
296
332
  };
297
- const buildAriaProps = (isTop, isModal, isComposable, ariaLabel, panelId, hasDescription) => {
333
+ const buildAriaProps = ({ ariaLabel, hasDescription, hasTitle, isComposable, isModal, isTop, panelId }) => {
298
334
  if (!isTop) return {};
299
335
  const props = { role: "dialog" };
300
336
  if (isModal) props["aria-modal"] = "true";
301
337
  if (isComposable) {
302
- props["aria-labelledby"] = `${panelId}-title`;
338
+ if (hasTitle) props["aria-labelledby"] = `${panelId}-title`;
339
+ else props["aria-label"] = ariaLabel;
303
340
  if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
304
341
  } else props["aria-label"] = ariaLabel;
305
342
  return props;
@@ -397,7 +434,7 @@ const RUBBER_BAND_FACTOR = .6;
397
434
  //#region src/Drag/drag-geometry.ts
398
435
  const isInteractiveElement = (el) => {
399
436
  if (INTERACTIVE_TAGS.has(el.tagName)) return true;
400
- if (el.isContentEditable) return true;
437
+ if (el instanceof HTMLElement && el.isContentEditable) return true;
401
438
  if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
402
439
  if (el.closest("[data-stacksheet-no-drag]")) return true;
403
440
  return false;
@@ -473,95 +510,108 @@ const classifyGesture = (dx, dy, axis, sign) => {
473
510
  /** Decide whether a pointer gesture commits as a drag or should be ignored. */
474
511
  const commitGesture = (dx, dy, axis, sign, scrollEl) => {
475
512
  if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
476
- if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
513
+ if (scrollEl !== null && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
477
514
  return "drag";
478
515
  };
479
516
  const getPanelDimension = (panel, axis) => {
480
517
  if (!panel) return 300;
481
518
  return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
482
519
  };
483
- //#endregion
484
- //#region src/Drag/use-drag.ts
520
+ /** Upper bound on retained samples — plenty for a 100ms window at 120Hz. */
521
+ const MAX_SAMPLES = 20;
485
522
  /**
486
- * Hook that manages drag gestures that can dismiss a sheet panel.
487
- *
488
- * Gesture pipeline:
489
- * 1. Dead zone (10px) ignores micro-movements
490
- * 2. Angle check (35°) — must be roughly aligned with dismiss axis
491
- * 3. Scroll conflict yields to scrollable containers not at edge
492
- * 4. Commit drag is active, applies offset via `onDragUpdate`
493
- * 5. Release — velocity + threshold determine close/snap/bounce-back
494
- *
495
- * Opposite-direction drag uses √(offset) damping for elastic
496
- * rubber-band resistance (same physics as iOS over-scroll).
523
+ * Append a pointer sample, pruning entries that fall outside the sliding
524
+ * window (plus a hard cap as a memory guard). Mutates and returns `samples`.
525
+ */
526
+ const appendVelocitySample = (samples, sample) => {
527
+ samples.push(sample);
528
+ const cutoff = sample.time - 100;
529
+ while (samples.length > MAX_SAMPLES || samples.length > 2 && (samples[0]?.time ?? 0) < cutoff) samples.shift();
530
+ return samples;
531
+ };
532
+ /**
533
+ * Compute release velocity (px/ms, positive = dismiss direction) from the
534
+ * samples recorded within the sliding window before `releaseTime`.
497
535
  *
498
- * When `snapHeights` is provided, release targeting uses
499
- * `findSnapTarget()` instead of the simple threshold check.
536
+ * Using only recent samples means a pause followed by a flick reports the
537
+ * flick's velocity not the whole-gesture average, which would dilute it
538
+ * to near zero.
500
539
  */
501
- const useDrag = (panelRef, config, onDragUpdate) => {
502
- const startRef = useRef(null);
503
- const committedRef = useRef(null);
504
- const offsetRef = useRef(0);
505
- const scrollTargetRef = useRef(null);
506
- const { axis, sign } = getDismissAxis(config.side);
540
+ const getReleaseVelocity = (samples, releaseTime) => {
541
+ const cutoff = releaseTime - 100;
542
+ const recent = samples.filter((sample) => sample.time >= cutoff);
543
+ const [first] = recent;
544
+ const last = recent.at(-1);
545
+ if (first === void 0 || last === void 0 || last.time <= first.time) return 0;
546
+ return (last.offset - first.offset) / (last.time - first.time);
547
+ };
548
+ //#endregion
549
+ //#region src/Drag/use-drag.ts
550
+ const resetDragRefs = ({ committedRef, offsetRef, samplesRef, scrollTargetRef, startRef }) => {
551
+ startRef.current = null;
552
+ committedRef.current = null;
553
+ offsetRef.current = 0;
554
+ samplesRef.current = [];
555
+ scrollTargetRef.current = null;
556
+ };
557
+ const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }) => {
558
+ const dismiss = () => {
559
+ if (config.isNested) config.onPop();
560
+ else config.onClose();
561
+ };
507
562
  const handlePointerDown = (e) => {
508
- if (!config.enabled) return;
509
- if (e.button !== 0) return;
510
- const target = e.target;
511
- if (!target) return;
512
- const isHandle = !!target.closest("[data-stacksheet-handle]");
563
+ if (!config.enabled || e.button !== 0 || !(e.target instanceof Element)) return;
564
+ const { target } = e;
565
+ const isHandle = target.closest("[data-stacksheet-handle]") !== null;
513
566
  if (!isHandle && isInteractiveElement(target)) return;
514
- scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
515
- startRef.current = {
516
- time: Date.now(),
567
+ refs.scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
568
+ refs.startRef.current = {
517
569
  x: e.clientX,
518
570
  y: e.clientY
519
571
  };
520
- committedRef.current = null;
521
- offsetRef.current = 0;
522
- e.currentTarget?.setPointerCapture?.(e.pointerId);
572
+ refs.committedRef.current = null;
573
+ refs.offsetRef.current = 0;
574
+ refs.samplesRef.current = [{
575
+ offset: 0,
576
+ time: Date.now()
577
+ }];
523
578
  };
524
579
  const handlePointerMove = (e) => {
525
- if (!startRef.current) return;
526
- const dx = e.clientX - startRef.current.x;
527
- const dy = e.clientY - startRef.current.y;
580
+ if (refs.startRef.current === null) return;
581
+ const dx = e.clientX - refs.startRef.current.x;
582
+ const dy = e.clientY - refs.startRef.current.y;
528
583
  const dist = Math.hypot(dx, dy);
529
- if (committedRef.current === null && dist < 10) return;
530
- if (committedRef.current === null) {
531
- committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
532
- if (committedRef.current !== "drag") {
533
- startRef.current = null;
584
+ if (refs.committedRef.current === null && dist < 10) return;
585
+ if (refs.committedRef.current === null) {
586
+ refs.committedRef.current = commitGesture(dx, dy, axis, sign, refs.scrollTargetRef.current);
587
+ if (refs.committedRef.current !== "drag") {
588
+ refs.startRef.current = null;
534
589
  return;
535
590
  }
591
+ if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
536
592
  }
537
- if (committedRef.current !== "drag") return;
593
+ if (refs.committedRef.current !== "drag") return;
538
594
  const directional = (axis === "x" ? dx : dy) * sign;
539
595
  const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
540
- offsetRef.current = clampedOffset;
596
+ refs.offsetRef.current = clampedOffset;
597
+ appendVelocitySample(refs.samplesRef.current, {
598
+ offset: clampedOffset,
599
+ time: Date.now()
600
+ });
541
601
  onDragUpdate({
542
602
  isDragging: true,
543
603
  offset: clampedOffset
544
604
  });
545
605
  e.preventDefault();
546
606
  };
547
- const dismiss = () => {
548
- if (config.isNested) config.onPop();
549
- else config.onClose();
550
- };
551
- const handlePointerUp = (_e) => {
552
- if (!startRef.current || committedRef.current !== "drag") {
553
- startRef.current = null;
554
- committedRef.current = null;
555
- scrollTargetRef.current = null;
607
+ const handlePointerUp = () => {
608
+ if (refs.startRef.current === null || refs.committedRef.current !== "drag") {
609
+ resetDragRefs(refs);
556
610
  return;
557
611
  }
558
- const offset = Math.max(0, offsetRef.current);
559
- const elapsed = Date.now() - startRef.current.time;
560
- const velocity = elapsed > 0 ? offset / elapsed : 0;
561
- startRef.current = null;
562
- committedRef.current = null;
563
- offsetRef.current = 0;
564
- scrollTargetRef.current = null;
612
+ const offset = Math.max(0, refs.offsetRef.current);
613
+ const velocity = getReleaseVelocity(refs.samplesRef.current, Date.now());
614
+ resetDragRefs(refs);
565
615
  const panelSize = getPanelDimension(panelRef.current, axis);
566
616
  if (config.snapHeights.length > 0) {
567
617
  const targetIndex = findSnapTarget(offset, panelSize, config.snapHeights, velocity, config.activeSnapIndex, config.sequential);
@@ -584,15 +634,56 @@ const useDrag = (panelRef, config, onDragUpdate) => {
584
634
  });
585
635
  };
586
636
  const handlePointerCancel = () => {
587
- startRef.current = null;
588
- committedRef.current = null;
589
- offsetRef.current = 0;
590
- scrollTargetRef.current = null;
637
+ resetDragRefs(refs);
591
638
  onDragUpdate({
592
639
  isDragging: false,
593
640
  offset: 0
594
641
  });
595
642
  };
643
+ return {
644
+ handlePointerCancel,
645
+ handlePointerDown,
646
+ handlePointerMove,
647
+ handlePointerUp
648
+ };
649
+ };
650
+ /**
651
+ * Hook that manages drag gestures that can dismiss a sheet panel.
652
+ *
653
+ * Gesture pipeline:
654
+ * 1. Dead zone (10px) — ignores micro-movements
655
+ * 2. Angle check (35°) — must be roughly aligned with dismiss axis
656
+ * 3. Scroll conflict — yields to scrollable containers not at edge
657
+ * 4. Commit — drag is active, applies offset via `onDragUpdate`
658
+ * 5. Release — velocity + threshold determine close/snap/bounce-back
659
+ *
660
+ * Opposite-direction drag uses √(offset) damping for elastic
661
+ * rubber-band resistance (same physics as iOS over-scroll).
662
+ *
663
+ * When `snapHeights` is provided, release targeting uses
664
+ * `findSnapTarget()` instead of the simple threshold check.
665
+ */
666
+ const useDrag = (panelRef, config, onDragUpdate) => {
667
+ const startRef = useRef(null);
668
+ const committedRef = useRef(null);
669
+ const offsetRef = useRef(0);
670
+ const samplesRef = useRef([]);
671
+ const scrollTargetRef = useRef(null);
672
+ const { axis, sign } = getDismissAxis(config.side);
673
+ const { handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp } = createDragHandlers({
674
+ axis,
675
+ config,
676
+ onDragUpdate,
677
+ panelRef,
678
+ refs: {
679
+ committedRef,
680
+ offsetRef,
681
+ samplesRef,
682
+ scrollTargetRef,
683
+ startRef
684
+ },
685
+ sign
686
+ });
596
687
  const handlersRef = useRef({
597
688
  handlePointerCancel,
598
689
  handlePointerDown,
@@ -607,20 +698,32 @@ const useDrag = (panelRef, config, onDragUpdate) => {
607
698
  };
608
699
  useEffect(() => {
609
700
  const el = panelRef.current;
610
- if (!(el && config.enabled)) return;
611
- const onPointerDown = (event) => handlersRef.current.handlePointerDown(event);
612
- const onPointerMove = (event) => handlersRef.current.handlePointerMove(event);
613
- const onPointerUp = (event) => handlersRef.current.handlePointerUp(event);
614
- const onPointerCancel = () => handlersRef.current.handlePointerCancel();
615
- el.addEventListener("pointerdown", onPointerDown);
616
- el.addEventListener("pointermove", onPointerMove);
617
- el.addEventListener("pointerup", onPointerUp);
618
- el.addEventListener("pointercancel", onPointerCancel);
701
+ const onPointerDown = (event) => {
702
+ handlersRef.current.handlePointerDown(event);
703
+ };
704
+ const onPointerMove = (event) => {
705
+ handlersRef.current.handlePointerMove(event);
706
+ };
707
+ const onPointerUp = () => {
708
+ handlersRef.current.handlePointerUp();
709
+ };
710
+ const onPointerCancel = () => {
711
+ handlersRef.current.handlePointerCancel();
712
+ };
713
+ const canListen = el !== null && config.enabled;
714
+ if (canListen) {
715
+ el.addEventListener("pointerdown", onPointerDown);
716
+ el.addEventListener("pointermove", onPointerMove);
717
+ el.addEventListener("pointerup", onPointerUp);
718
+ el.addEventListener("pointercancel", onPointerCancel);
719
+ }
619
720
  return () => {
620
- el.removeEventListener("pointerdown", onPointerDown);
621
- el.removeEventListener("pointermove", onPointerMove);
622
- el.removeEventListener("pointerup", onPointerUp);
623
- el.removeEventListener("pointercancel", onPointerCancel);
721
+ if (canListen) {
722
+ el.removeEventListener("pointerdown", onPointerDown);
723
+ el.removeEventListener("pointermove", onPointerMove);
724
+ el.removeEventListener("pointerup", onPointerUp);
725
+ el.removeEventListener("pointercancel", onPointerCancel);
726
+ }
624
727
  };
625
728
  }, [panelRef, config.enabled]);
626
729
  };
@@ -795,11 +898,12 @@ const DefaultHeader = ({ isNested, onBack, onClose, className }) => /* @__PURE__
795
898
  //#endregion
796
899
  //#region src/SheetPanel/sheet-panel-content.tsx
797
900
  const PanelInnerContent = ({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) => {
798
- if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
799
- return /* @__PURE__ */ jsxs(Fragment, { children: [renderHeader ? renderHeader(headerProps) : /* @__PURE__ */ jsx(DefaultHeader, {
901
+ if (isComposable) return shouldRender && Content !== void 0 ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
902
+ const customHeader = renderHeader !== void 0 && renderHeader !== false ? renderHeader : void 0;
903
+ return /* @__PURE__ */ jsxs(Fragment, { children: [customHeader === void 0 ? /* @__PURE__ */ jsx(DefaultHeader, {
800
904
  ...headerProps,
801
905
  className: headerClassName
802
- }), shouldRender && Content && /* @__PURE__ */ jsx("div", {
906
+ }) : customHeader(headerProps), shouldRender && Content !== void 0 && /* @__PURE__ */ jsx("div", {
803
907
  className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
804
908
  "data-stacksheet-no-drag": "",
805
909
  children: /* @__PURE__ */ jsx(Content, { ...data })
@@ -816,7 +920,9 @@ const LAYERED_MODAL_SELECTORS = [
816
920
  ].join(", ");
817
921
  const subscribeToFocusTarget = (onStoreChange) => {
818
922
  document.addEventListener("focusin", onStoreChange, true);
819
- return () => document.removeEventListener("focusin", onStoreChange, true);
923
+ return () => {
924
+ document.removeEventListener("focusin", onStoreChange, true);
925
+ };
820
926
  };
821
927
  const getServerLayeredModalFocused = () => false;
822
928
  const getLayeredModalFocused = () => {
@@ -837,10 +943,10 @@ const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
837
943
  allowOutsideClick: true,
838
944
  escapeDeactivates: false,
839
945
  fallbackFocus: () => {
840
- if (fallbackRef.current) return fallbackRef.current;
946
+ if (fallbackRef.current !== null) return fallbackRef.current;
841
947
  return document.body;
842
948
  },
843
- initialFocus: false,
949
+ initialFocus: () => fallbackRef.current ?? void 0,
844
950
  returnFocusOnDeactivate: true
845
951
  },
846
952
  paused,
@@ -890,10 +996,18 @@ const resolvePanelLayout = (layout, renderHeader) => {
890
996
  //#region src/SheetPanel/sheet-panel.tsx
891
997
  const getPanelAriaLabel = (item, fallbackLabel) => item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? fallbackLabel;
892
998
  const getPanelHoverProps = (enabled, setIsHovered) => enabled ? {
893
- onBlur: () => setIsHovered(false),
894
- onFocus: () => setIsHovered(true),
895
- onMouseEnter: () => setIsHovered(true),
896
- onMouseLeave: () => setIsHovered(false)
999
+ onBlur: () => {
1000
+ setIsHovered(false);
1001
+ },
1002
+ onFocus: () => {
1003
+ setIsHovered(true);
1004
+ },
1005
+ onMouseEnter: () => {
1006
+ setIsHovered(true);
1007
+ },
1008
+ onMouseLeave: () => {
1009
+ setIsHovered(false);
1010
+ }
897
1011
  } : {};
898
1012
  const getInactivePanelProps = (isTop) => isTop ? {} : {
899
1013
  "aria-hidden": "true",
@@ -905,14 +1019,16 @@ const getHeaderProps = ({ close, isNested, pop, side }) => ({
905
1019
  onClose: close,
906
1020
  side
907
1021
  });
908
- const getPanelContext = ({ close, hasDescription, isNested, isTop, panelId, pop, registerDescription, side }) => ({
1022
+ const getPanelContext = ({ close, hasDescription, hasTitle, isNested, isTop, panelId, pop, registerDescription, registerTitle, side }) => ({
909
1023
  back: pop,
910
1024
  close,
911
1025
  hasDescription,
1026
+ hasTitle,
912
1027
  isNested,
913
1028
  isTop,
914
1029
  panelId,
915
1030
  registerDescription,
1031
+ registerTitle,
916
1032
  side
917
1033
  });
918
1034
  const getOptionalSideHandle = ({ isHovered, onDismiss, show, side }) => show ? /* @__PURE__ */ jsx(SideHandle, {
@@ -927,25 +1043,22 @@ const completeOpeningAnimation = (hasEnteredRef, isTop, onOpenCompleteRef) => {
927
1043
  onOpenCompleteRef.current?.();
928
1044
  }
929
1045
  };
930
- const 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 }) => {
931
- const panelRef = useRef(null);
1046
+ const useOpeningCompletion = (isTop, onOpenComplete) => {
932
1047
  const hasEnteredRef = useRef(false);
933
- const onOpenCompleteRef = useRef(config.onOpenComplete);
934
- const [dragState, setDragState] = useState({
935
- isDragging: false,
936
- offset: 0
937
- });
938
- const [isHovered, setIsHovered] = useState(false);
939
- const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
940
- const transform = getStackTransform(depth, config.stacking);
941
- const panelStyles = getPanelStyles(side, config, index);
1048
+ const onOpenCompleteRef = useRef(onOpenComplete);
942
1049
  if (!isTop && hasEnteredRef.current) hasEnteredRef.current = false;
943
1050
  useEffect(() => {
944
- onOpenCompleteRef.current = config.onOpenComplete;
945
- }, [config.onOpenComplete]);
946
- const handleAnimationComplete = () => {
1051
+ onOpenCompleteRef.current = onOpenComplete;
1052
+ }, [onOpenComplete]);
1053
+ return () => {
947
1054
  completeOpeningAnimation(hasEnteredRef, isTop, onOpenCompleteRef);
948
1055
  };
1056
+ };
1057
+ const useSheetPanelDrag = ({ activeSnapIndex, config, isNested, isTop, onSnap, prefersReducedMotion, side, snapHeights, swipeClose, swipePop }, panelRef) => {
1058
+ const [dragState, setDragState] = useState({
1059
+ isDragging: false,
1060
+ offset: 0
1061
+ });
949
1062
  useDrag(panelRef, {
950
1063
  activeSnapIndex,
951
1064
  closeThreshold: config.closeThreshold,
@@ -959,23 +1072,63 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
959
1072
  snapHeights,
960
1073
  velocityThreshold: config.velocityThreshold
961
1074
  }, setDragState);
962
- const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
963
- const panelId = `stacksheet-${item.id}`;
1075
+ return dragState;
1076
+ };
1077
+ const useSheetPanelContext = ({ close, isNested, isTop, pop, side }, panelId) => {
964
1078
  const [hasDescription, setHasDescription] = useState(false);
965
- const registerDescription = () => {
1079
+ const [hasTitle, setHasTitle] = useState(false);
1080
+ const registerDescription = useCallback(() => {
966
1081
  setHasDescription(true);
967
- return () => setHasDescription(false);
968
- };
969
- const panelContext = getPanelContext({
970
- close,
1082
+ return () => {
1083
+ setHasDescription(false);
1084
+ };
1085
+ }, []);
1086
+ const registerTitle = useCallback(() => {
1087
+ setHasTitle(true);
1088
+ return () => {
1089
+ setHasTitle(false);
1090
+ };
1091
+ }, []);
1092
+ return {
971
1093
  hasDescription,
972
- isNested,
973
- isTop,
974
- panelId,
975
- pop,
976
- registerDescription,
977
- side
978
- });
1094
+ hasTitle,
1095
+ panelContext: useMemo(() => getPanelContext({
1096
+ close,
1097
+ hasDescription,
1098
+ hasTitle,
1099
+ isNested,
1100
+ isTop,
1101
+ panelId,
1102
+ pop,
1103
+ registerDescription,
1104
+ registerTitle,
1105
+ side
1106
+ }), [
1107
+ close,
1108
+ hasDescription,
1109
+ hasTitle,
1110
+ isNested,
1111
+ isTop,
1112
+ panelId,
1113
+ pop,
1114
+ registerDescription,
1115
+ registerTitle,
1116
+ side
1117
+ ])
1118
+ };
1119
+ };
1120
+ const useSheetPanelModel = (props) => {
1121
+ const { item, index, depth, isTop, isNested, side, config, classNames, pop, close, snapHeights, activeSnapIndex, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring } = props;
1122
+ const panelRef = useRef(null);
1123
+ const [isHovered, setIsHovered] = useState(false);
1124
+ const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
1125
+ const transform = getStackTransform(depth, config.stacking);
1126
+ const panelStyles = getPanelStyles(side, config, index);
1127
+ const handleAnimationComplete = useOpeningCompletion(isTop, config.onOpenComplete);
1128
+ const dragState = useSheetPanelDrag(props, panelRef);
1129
+ const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
1130
+ const panelId = `stacksheet-${item.id}`;
1131
+ const { hasDescription, hasTitle, panelContext } = useSheetPanelContext(props, panelId);
979
1132
  const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
980
1133
  const hasPanelClass = classNames.panel !== "";
981
1134
  const dragOffset = getDragTransform(side, dragState.offset);
@@ -986,7 +1139,15 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
986
1139
  pop,
987
1140
  side
988
1141
  });
989
- const ariaProps = buildAriaProps(isTop, config.modal, isComposable, ariaLabel, panelId, hasDescription);
1142
+ const ariaProps = buildAriaProps({
1143
+ ariaLabel,
1144
+ hasDescription,
1145
+ hasTitle,
1146
+ isComposable,
1147
+ isModal: config.modal,
1148
+ isTop,
1149
+ panelId
1150
+ });
990
1151
  const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
991
1152
  const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
992
1153
  const snapYOffset = computeSnapYOffset(side, snapHeights, activeSnapIndex, measuredHeight);
@@ -1002,9 +1163,26 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
1002
1163
  show: showSideHandle,
1003
1164
  side
1004
1165
  });
1005
- const bottomHandle = getOptionalBottomHandle(showBottomHandle, dismiss);
1006
- const hoverProps = getPanelHoverProps(showSideHandle, setIsHovered);
1007
- const inactivePanelProps = getInactivePanelProps(isTop);
1166
+ return {
1167
+ animateTarget,
1168
+ ariaProps,
1169
+ bottomHandle: getOptionalBottomHandle(showBottomHandle, dismiss),
1170
+ handleAnimationComplete,
1171
+ headerProps,
1172
+ hoverProps: getPanelHoverProps(showSideHandle, setIsHovered),
1173
+ inactivePanelProps: getInactivePanelProps(isTop),
1174
+ initialRadius,
1175
+ isComposable,
1176
+ panelContext,
1177
+ panelRef,
1178
+ panelStyle,
1179
+ resolvedSlideFrom,
1180
+ sideHandle
1181
+ };
1182
+ };
1183
+ const SheetPanel = (props) => {
1184
+ const { item, isTop, config, classNames, Content, shouldRender, renderHeader, prefersReducedMotion } = props;
1185
+ const { animateTarget, ariaProps, bottomHandle, handleAnimationComplete, headerProps, hoverProps, inactivePanelProps, initialRadius, isComposable, panelContext, panelRef, panelStyle, resolvedSlideFrom, sideHandle } = useSheetPanelModel(props);
1008
1186
  const panelContent = /* @__PURE__ */ jsxs(m.div, {
1009
1187
  animate: animateTarget,
1010
1188
  className: classNames.panel || void 0,
@@ -1079,21 +1257,27 @@ const getBackdropStyle = (config, hasBackdropClass) => ({
1079
1257
  zIndex: config.zIndex,
1080
1258
  ...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
1081
1259
  });
1082
- /**
1083
- * Root renderer component — manages the backdrop, scroll lock, snap points,
1084
- * close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
1085
- * delegates per-panel rendering to `SheetPanel`.
1086
- *
1087
- * Mounted inside a Portal by `StacksheetProvider`.
1088
- */
1089
- const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
1260
+ const isContentComponent = (content) => typeof content === "function";
1261
+ const getStaticContent = (sheets, type) => {
1262
+ if (sheets === void 0) return;
1263
+ const content = Object.entries(sheets).find(([sheetType]) => sheetType === type)?.[1];
1264
+ return isContentComponent(content) ? content : void 0;
1265
+ };
1266
+ const useRendererStore = (store) => {
1090
1267
  const isOpen = useStore(store, (s) => s.isOpen);
1091
1268
  const stack = useStore(store, (s) => s.stack);
1092
- const rawClose = useStore(store, (s) => s.close);
1093
- const rawPop = useStore(store, (s) => s.pop);
1094
- const side = useResolvedSide(config);
1095
- const prefersReducedMotion = useReducedMotion() ?? false;
1096
- const classNames = resolveClassNames(classNamesProp);
1269
+ const { rawClose, rawPop } = useStore(store, useShallow((s) => ({
1270
+ rawClose: s.close,
1271
+ rawPop: s.pop
1272
+ })));
1273
+ return {
1274
+ isOpen,
1275
+ rawClose,
1276
+ rawPop,
1277
+ stack
1278
+ };
1279
+ };
1280
+ const useSnapState = (config, isOpen, side, stack) => {
1097
1281
  const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
1098
1282
  const snapHeights = getResolvedSnapHeights(side, config.snapPoints, viewportHeight);
1099
1283
  const snapContext = isOpen ? stack.map((item) => item.id).join("\0") : "";
@@ -1118,6 +1302,13 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1118
1302
  });
1119
1303
  config.onSnapPointChange?.(index);
1120
1304
  };
1305
+ return {
1306
+ activeSnapIndex,
1307
+ handleSnap,
1308
+ snapHeights
1309
+ };
1310
+ };
1311
+ const useCloseControls = (rawClose, rawPop) => {
1121
1312
  const closeReasonRef = useRef("programmatic");
1122
1313
  const closeWith = (reason) => {
1123
1314
  closeReasonRef.current = reason;
@@ -1127,9 +1318,19 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1127
1318
  closeReasonRef.current = reason;
1128
1319
  rawPop();
1129
1320
  };
1130
- const close = () => closeWith("programmatic");
1131
- const pop = () => popWith("programmatic");
1132
- useBodyScale(config, isOpen, prefersReducedMotion);
1321
+ return {
1322
+ close: () => {
1323
+ closeWith("programmatic");
1324
+ },
1325
+ closeReasonRef,
1326
+ closeWith,
1327
+ pop: () => {
1328
+ popWith("programmatic");
1329
+ },
1330
+ popWith
1331
+ };
1332
+ };
1333
+ const useFocusRestore = (isOpen) => {
1133
1334
  const triggerRef = useRef(null);
1134
1335
  const wasOpenRef = useRef(false);
1135
1336
  useEffect(() => {
@@ -1141,56 +1342,96 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1141
1342
  }
1142
1343
  wasOpenRef.current = isOpen;
1143
1344
  }, [isOpen]);
1144
- const stackLengthRef = useRef(stack.length);
1345
+ };
1346
+ const dismissFromEscape = ({ closeReasonRef, rawClose, rawPop, stackLengthRef }) => {
1347
+ closeReasonRef.current = "escape";
1348
+ if (stackLengthRef.current > 1) rawPop();
1349
+ else rawClose();
1350
+ };
1351
+ const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop, stackLength }) => {
1352
+ const stackLengthRef = useRef(stackLength);
1145
1353
  useEffect(() => {
1146
- stackLengthRef.current = stack.length;
1147
- }, [stack.length]);
1354
+ stackLengthRef.current = stackLength;
1355
+ }, [stackLength]);
1148
1356
  useEffect(() => {
1149
- if (!(isOpen && config.closeOnEscape && config.dismissible)) return;
1357
+ const shouldListen = isOpen && config.closeOnEscape && config.dismissible;
1150
1358
  const handleKeyDown = (e) => {
1151
- if (e.key === "Escape") {
1152
- e.preventDefault();
1153
- if (stackLengthRef.current > 1) {
1154
- closeReasonRef.current = "escape";
1155
- rawPop();
1156
- } else {
1157
- closeReasonRef.current = "escape";
1158
- rawClose();
1159
- }
1160
- }
1359
+ if (e.key !== "Escape" || e.defaultPrevented) return;
1360
+ e.preventDefault();
1361
+ dismissFromEscape({
1362
+ closeReasonRef,
1363
+ rawClose,
1364
+ rawPop,
1365
+ stackLengthRef
1366
+ });
1367
+ };
1368
+ if (shouldListen) document.addEventListener("keydown", handleKeyDown);
1369
+ return () => {
1370
+ if (shouldListen) document.removeEventListener("keydown", handleKeyDown);
1161
1371
  };
1162
- document.addEventListener("keydown", handleKeyDown);
1163
- return () => document.removeEventListener("keydown", handleKeyDown);
1164
1372
  }, [
1165
1373
  isOpen,
1166
1374
  config.closeOnEscape,
1167
1375
  config.dismissible,
1168
1376
  rawPop,
1169
- rawClose
1377
+ rawClose,
1378
+ closeReasonRef
1170
1379
  ]);
1171
1380
  useEffect(() => {
1172
- if (!(isOpen && config.dismissible) || globalThis.CloseWatcher === void 0) return;
1173
- const watcher = new globalThis.CloseWatcher();
1381
+ const CloseWatcherConstructor = globalThis.CloseWatcher;
1382
+ const shouldListen = isOpen && config.closeOnEscape && config.dismissible && CloseWatcherConstructor !== void 0;
1383
+ let watcher;
1174
1384
  const handleClose = () => {
1175
- if (stackLengthRef.current > 1) {
1176
- closeReasonRef.current = "escape";
1177
- rawPop();
1178
- } else {
1179
- closeReasonRef.current = "escape";
1180
- rawClose();
1181
- }
1385
+ dismissFromEscape({
1386
+ closeReasonRef,
1387
+ rawClose,
1388
+ rawPop,
1389
+ stackLengthRef
1390
+ });
1182
1391
  };
1183
- watcher.addEventListener("close", handleClose);
1392
+ if (shouldListen) {
1393
+ watcher = new CloseWatcherConstructor();
1394
+ watcher.addEventListener("close", handleClose);
1395
+ }
1184
1396
  return () => {
1185
- watcher.removeEventListener("close", handleClose);
1186
- watcher.destroy();
1397
+ if (watcher !== void 0) {
1398
+ watcher.removeEventListener("close", handleClose);
1399
+ watcher.destroy();
1400
+ }
1187
1401
  };
1188
1402
  }, [
1189
1403
  isOpen,
1404
+ config.closeOnEscape,
1190
1405
  config.dismissible,
1191
1406
  rawPop,
1192
- rawClose
1407
+ rawClose,
1408
+ closeReasonRef
1193
1409
  ]);
1410
+ };
1411
+ /**
1412
+ * Root renderer component — manages the backdrop, scroll lock, snap points,
1413
+ * close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
1414
+ * delegates per-panel rendering to `SheetPanel`.
1415
+ *
1416
+ * Mounted inside a Portal by `StacksheetProvider`.
1417
+ */
1418
+ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
1419
+ const { isOpen, rawClose, rawPop, stack } = useRendererStore(store);
1420
+ const side = useResolvedSide(config);
1421
+ const prefersReducedMotion = useReducedMotion() ?? false;
1422
+ const classNames = resolveClassNames(classNamesProp);
1423
+ const { activeSnapIndex, handleSnap, snapHeights } = useSnapState(config, isOpen, side, stack);
1424
+ const { close, closeReasonRef, closeWith, pop, popWith } = useCloseControls(rawClose, rawPop);
1425
+ useBodyScale(config, isOpen, prefersReducedMotion);
1426
+ useFocusRestore(isOpen);
1427
+ useDismissalEffects({
1428
+ closeReasonRef,
1429
+ config,
1430
+ isOpen,
1431
+ rawClose,
1432
+ rawPop,
1433
+ stackLength: stack.length
1434
+ });
1194
1435
  const slideFrom = getSlideFrom(side);
1195
1436
  const slideTarget = getSlideTarget();
1196
1437
  const spring = getMotionSpring(prefersReducedMotion, config);
@@ -1201,8 +1442,12 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1201
1442
  const handleExitComplete = () => {
1202
1443
  if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
1203
1444
  };
1204
- const swipeClose = () => closeWith("swipe");
1205
- const swipePop = () => popWith("swipe");
1445
+ const swipeClose = () => {
1446
+ closeWith("swipe");
1447
+ };
1448
+ const swipePop = () => {
1449
+ popWith("swipe");
1450
+ };
1206
1451
  const shouldLockScroll = isOpen && isModal && config.lockScroll;
1207
1452
  return /* @__PURE__ */ jsxs(LazyMotion, {
1208
1453
  features: domMax,
@@ -1213,7 +1458,9 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1213
1458
  className: `fixed inset-0 ${classNames.backdrop || ""}`,
1214
1459
  exit: { opacity: 0 },
1215
1460
  initial: { opacity: 0 },
1216
- onClick: config.closeOnBackdrop && config.dismissible ? () => closeWith("backdrop") : void 0,
1461
+ onClick: config.closeOnBackdrop && config.dismissible ? () => {
1462
+ closeWith("backdrop");
1463
+ } : void 0,
1217
1464
  style: backdropStyle,
1218
1465
  transition: spring
1219
1466
  }, "stacksheet-backdrop")
@@ -1232,7 +1479,7 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1232
1479
  const shouldRender = depth <= config.stacking.renderThreshold;
1233
1480
  return /* @__PURE__ */ jsx(SheetPanel, {
1234
1481
  activeSnapIndex,
1235
- Content: componentMap.get(item.type) ?? sheets[item.type],
1482
+ Content: componentMap.get(item.type) ?? getStaticContent(sheets, item.type),
1236
1483
  classNames,
1237
1484
  close,
1238
1485
  config,
@@ -1264,11 +1511,32 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1264
1511
  };
1265
1512
  //#endregion
1266
1513
  //#region src/Store/store-args.ts
1514
+ const isRecord = (value) => typeof value === "object" && value !== null;
1515
+ const toRecord = (value) => isRecord(value) ? value : {};
1516
+ const isComponent = (value) => typeof value === "function";
1517
+ const getComponentName = (component) => {
1518
+ const name = component.displayName ?? component.name;
1519
+ return name === "" ? void 0 : name;
1520
+ };
1521
+ const getNodeEnv = () => globalThis.process?.env?.NODE_ENV;
1522
+ /**
1523
+ * Generate a unique sheet id. `crypto.randomUUID` is only available in
1524
+ * secure contexts, so fall back to a timestamp + random suffix on
1525
+ * non-secure origins (e.g. plain-HTTP LAN dev servers).
1526
+ */
1527
+ const generateSheetId = () => {
1528
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
1529
+ return `sheet-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1530
+ };
1531
+ const getStringArg = (value, name) => {
1532
+ if (typeof value !== "string") throw new TypeError(`Expected ${name} to be a string.`);
1533
+ return value;
1534
+ };
1267
1535
  const resolvePresentationOptions = (value) => {
1268
- if (!(value && typeof value === "object")) return;
1269
- const candidate = value;
1270
- if (candidate.ariaLabel !== void 0 && typeof candidate.ariaLabel !== "string") return;
1271
- return candidate;
1536
+ if (!isRecord(value)) return;
1537
+ const { ariaLabel } = value;
1538
+ if (ariaLabel !== void 0 && typeof ariaLabel !== "string") return;
1539
+ return ariaLabel === void 0 ? {} : { ariaLabel };
1272
1540
  };
1273
1541
  /**
1274
1542
  * Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
@@ -1276,52 +1544,202 @@ const resolvePresentationOptions = (value) => {
1276
1544
  * it's almost always an inline arrow being re-created every render.
1277
1545
  */
1278
1546
  const warnInlineComponent = (component, componentRegistry, warnedNames) => {
1279
- if (process === void 0 || process?.env?.NODE_ENV === "production") return;
1280
- const name = component.displayName || component.name;
1281
- if (!name) return;
1547
+ if (getNodeEnv() === "production") return;
1548
+ const name = getComponentName(component);
1549
+ if (name === void 0) return;
1282
1550
  if (warnedNames.has(name)) return;
1283
- for (const [existing, key] of componentRegistry) if ((existing.displayName || existing.name) === name) {
1551
+ for (const [existing, key] of componentRegistry) if (getComponentName(existing) === name) {
1284
1552
  warnedNames.add(name);
1285
1553
  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.`);
1286
1554
  return;
1287
1555
  }
1288
1556
  };
1557
+ const registerComponent = (component, componentRegistry, componentMap, getNextKey, warnedNames) => {
1558
+ const existingKey = componentRegistry.get(component);
1559
+ if (existingKey !== void 0) return existingKey;
1560
+ warnInlineComponent(component, componentRegistry, warnedNames);
1561
+ const nextKey = getNextKey();
1562
+ componentRegistry.set(component, nextKey);
1563
+ componentMap.set(nextKey, component);
1564
+ return nextKey;
1565
+ };
1289
1566
  /**
1290
1567
  * If `first` is a function (component), register it and return { type, id, data }.
1291
1568
  * Otherwise, pass through the string-based (type, id, data) args unchanged.
1292
1569
  */
1293
1570
  const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) => {
1294
- if (typeof first === "function") {
1295
- const component = first;
1296
- let typeKey = componentRegistry.get(component);
1297
- if (!typeKey) {
1298
- warnInlineComponent(component, componentRegistry, warnedNames);
1299
- typeKey = getNextKey();
1300
- componentRegistry.set(component, typeKey);
1301
- componentMap.set(typeKey, component);
1302
- }
1571
+ if (isComponent(first)) {
1572
+ const typeKey = registerComponent(first, componentRegistry, componentMap, getNextKey, warnedNames);
1303
1573
  if (typeof second === "string") return {
1304
1574
  ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1305
- data: third ?? {},
1575
+ data: toRecord(third),
1306
1576
  id: second,
1307
1577
  type: typeKey
1308
1578
  };
1309
1579
  return {
1310
1580
  ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
1311
- data: second ?? {},
1312
- id: crypto.randomUUID(),
1581
+ data: toRecord(second),
1582
+ id: generateSheetId(),
1313
1583
  type: typeKey
1314
1584
  };
1315
1585
  }
1316
1586
  return {
1317
1587
  ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1318
- data: third ?? {},
1319
- id: second,
1320
- type: first
1588
+ data: toRecord(third),
1589
+ id: getStringArg(second, "sheet id"),
1590
+ type: getStringArg(first, "sheet type")
1321
1591
  };
1322
1592
  };
1323
1593
  //#endregion
1324
1594
  //#region src/Store/create-sheet-store.ts
1595
+ const createItem = ({ type, id, data, ariaLabel }) => ({
1596
+ ariaLabel,
1597
+ data,
1598
+ id,
1599
+ type
1600
+ });
1601
+ const createResolvedWriters = (set, config) => ({
1602
+ openResolved: (resolved) => {
1603
+ set({
1604
+ isOpen: true,
1605
+ stack: [createItem(resolved)]
1606
+ });
1607
+ },
1608
+ pushResolved: (resolved) => {
1609
+ set((state) => {
1610
+ const item = createItem(resolved);
1611
+ if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
1612
+ isOpen: true,
1613
+ stack: [...state.stack.slice(0, -1), item]
1614
+ };
1615
+ return {
1616
+ isOpen: true,
1617
+ stack: [...state.stack, item]
1618
+ };
1619
+ });
1620
+ },
1621
+ replaceResolved: (resolved) => {
1622
+ set((state) => {
1623
+ const item = createItem(resolved);
1624
+ if (state.stack.length === 0) return {
1625
+ isOpen: true,
1626
+ stack: [item]
1627
+ };
1628
+ return {
1629
+ isOpen: true,
1630
+ stack: [...state.stack.slice(0, -1), item]
1631
+ };
1632
+ });
1633
+ }
1634
+ });
1635
+ const createNavigate = (componentMap, get, resolve, writers) => (first, second, third, fourth) => {
1636
+ const resolved = resolve(first, second, third, fourth);
1637
+ const { stack } = get();
1638
+ const top = stack.at(-1);
1639
+ if (stack.length === 0) {
1640
+ writers.openResolved(resolved);
1641
+ return;
1642
+ }
1643
+ let isSameType = top?.type === resolved.type;
1644
+ if (!isSameType && isComponent(first)) isSameType = componentMap.get(top?.type ?? "") === first;
1645
+ if (isSameType) {
1646
+ writers.replaceResolved(resolved);
1647
+ return;
1648
+ }
1649
+ writers.pushResolved(resolved);
1650
+ };
1651
+ const createPop = (set, pruneRegistry) => () => {
1652
+ set((state) => {
1653
+ if (state.stack.length <= 1) {
1654
+ pruneRegistry([]);
1655
+ return {
1656
+ isOpen: false,
1657
+ stack: []
1658
+ };
1659
+ }
1660
+ const next = state.stack.slice(0, -1);
1661
+ pruneRegistry(next);
1662
+ return {
1663
+ isOpen: true,
1664
+ stack: next
1665
+ };
1666
+ });
1667
+ };
1668
+ const createRemove = (set, pruneRegistry) => (id) => {
1669
+ set((state) => {
1670
+ const next = state.stack.filter((item) => item.id !== id);
1671
+ if (next.length === state.stack.length) return state;
1672
+ pruneRegistry(next);
1673
+ return next.length === 0 ? {
1674
+ isOpen: false,
1675
+ stack: []
1676
+ } : { stack: next };
1677
+ });
1678
+ };
1679
+ const createSetData = (set, resolve) => (first, second, third) => {
1680
+ const { id, data } = resolve(first, second, third);
1681
+ set((state) => {
1682
+ const index = state.stack.findIndex((item) => item.id === id);
1683
+ const existing = state.stack[index];
1684
+ if (existing === void 0) return state;
1685
+ const updated = [...state.stack];
1686
+ updated[index] = {
1687
+ ...existing,
1688
+ data
1689
+ };
1690
+ return { stack: updated };
1691
+ });
1692
+ };
1693
+ const createSwap = (set, internals) => (first, second, third) => {
1694
+ const type = isComponent(first) ? registerComponent(first, internals.componentRegistry, internals.componentMap, internals.getNextKey, internals.warnedNames) : getStringArg(first, "sheet type");
1695
+ const data = toRecord(second);
1696
+ const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
1697
+ set((state) => {
1698
+ const top = state.stack.at(-1);
1699
+ if (top === void 0) return state;
1700
+ const newStack = [...state.stack];
1701
+ newStack[newStack.length - 1] = {
1702
+ ariaLabel: ariaLabel ?? top.ariaLabel,
1703
+ data,
1704
+ id: top.id,
1705
+ type
1706
+ };
1707
+ return { stack: newStack };
1708
+ });
1709
+ };
1710
+ const createStoreState = ({ componentMap, componentRegistry, config, getNextKey, pruneRegistry, resolve, warnedNames }) => (set, get) => {
1711
+ const writers = createResolvedWriters(set, config);
1712
+ return {
1713
+ close: () => {
1714
+ pruneRegistry([]);
1715
+ set({
1716
+ isOpen: false,
1717
+ stack: []
1718
+ });
1719
+ },
1720
+ isOpen: false,
1721
+ navigate: createNavigate(componentMap, get, resolve, writers),
1722
+ open: (first, second, third, fourth) => {
1723
+ writers.openResolved(resolve(first, second, third, fourth));
1724
+ },
1725
+ pop: createPop(set, pruneRegistry),
1726
+ push: (first, second, third, fourth) => {
1727
+ writers.pushResolved(resolve(first, second, third, fourth));
1728
+ },
1729
+ remove: createRemove(set, pruneRegistry),
1730
+ replace: (first, second, third, fourth) => {
1731
+ writers.replaceResolved(resolve(first, second, third, fourth));
1732
+ },
1733
+ setData: createSetData(set, resolve),
1734
+ stack: [],
1735
+ swap: createSwap(set, {
1736
+ componentMap,
1737
+ componentRegistry,
1738
+ getNextKey,
1739
+ warnedNames
1740
+ })
1741
+ };
1742
+ };
1325
1743
  /**
1326
1744
  * Create an isolated Zustand store for a sheet stack instance.
1327
1745
  *
@@ -1355,165 +1773,15 @@ const createSheetStore = (config) => {
1355
1773
  return {
1356
1774
  componentMap,
1357
1775
  componentRegistry,
1358
- store: createStore()((set, get) => {
1359
- const _openResolved = ({ type, id, data, ariaLabel }) => {
1360
- set({
1361
- isOpen: true,
1362
- stack: [{
1363
- ariaLabel,
1364
- data,
1365
- id,
1366
- type
1367
- }]
1368
- });
1369
- };
1370
- const _pushResolved = ({ type, id, data, ariaLabel }) => {
1371
- set((state) => {
1372
- const item = {
1373
- ariaLabel,
1374
- data,
1375
- id,
1376
- type
1377
- };
1378
- if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
1379
- isOpen: true,
1380
- stack: [...state.stack.slice(0, -1), item]
1381
- };
1382
- return {
1383
- isOpen: true,
1384
- stack: [...state.stack, item]
1385
- };
1386
- });
1387
- };
1388
- const _replaceResolved = ({ type, id, data, ariaLabel }) => {
1389
- set((state) => {
1390
- const item = {
1391
- ariaLabel,
1392
- data,
1393
- id,
1394
- type
1395
- };
1396
- if (state.stack.length === 0) return {
1397
- isOpen: true,
1398
- stack: [item]
1399
- };
1400
- return {
1401
- isOpen: true,
1402
- stack: [...state.stack.slice(0, -1), item]
1403
- };
1404
- });
1405
- };
1406
- return {
1407
- close: () => {
1408
- pruneRegistry([]);
1409
- set({
1410
- isOpen: false,
1411
- stack: []
1412
- });
1413
- },
1414
- isOpen: false,
1415
- navigate: (first, second, third, fourth) => {
1416
- const resolved = resolve(first, second, third, fourth);
1417
- const { stack } = get();
1418
- const top = stack.at(-1);
1419
- if (stack.length === 0) {
1420
- _openResolved(resolved);
1421
- return;
1422
- }
1423
- let isSameType = top?.type === resolved.type;
1424
- if (!isSameType && typeof first === "function") isSameType = componentMap.get(top?.type ?? "") === first;
1425
- if (isSameType) {
1426
- _replaceResolved(resolved);
1427
- return;
1428
- }
1429
- _pushResolved(resolved);
1430
- },
1431
- open: (first, second, third, fourth) => {
1432
- _openResolved(resolve(first, second, third, fourth));
1433
- },
1434
- pop: () => {
1435
- set((state) => {
1436
- if (state.stack.length <= 1) {
1437
- pruneRegistry([]);
1438
- return {
1439
- isOpen: false,
1440
- stack: []
1441
- };
1442
- }
1443
- const next = state.stack.slice(0, -1);
1444
- pruneRegistry(next);
1445
- return {
1446
- isOpen: true,
1447
- stack: next
1448
- };
1449
- });
1450
- },
1451
- push: (first, second, third, fourth) => {
1452
- _pushResolved(resolve(first, second, third, fourth));
1453
- },
1454
- remove: (id) => {
1455
- set((state) => {
1456
- const next = state.stack.filter((item) => item.id !== id);
1457
- if (next.length === state.stack.length) return state;
1458
- pruneRegistry(next);
1459
- if (next.length === 0) return {
1460
- isOpen: false,
1461
- stack: []
1462
- };
1463
- return { stack: next };
1464
- });
1465
- },
1466
- replace: (first, second, third, fourth) => {
1467
- _replaceResolved(resolve(first, second, third, fourth));
1468
- },
1469
- setData: (first, second, third) => {
1470
- const { id, data } = resolve(first, second, third);
1471
- set((state) => {
1472
- const idx = state.stack.findIndex((item) => item.id === id);
1473
- if (idx === -1) return state;
1474
- const updated = [...state.stack];
1475
- updated[idx] = {
1476
- ...updated[idx],
1477
- data
1478
- };
1479
- return { stack: updated };
1480
- });
1481
- },
1482
- stack: [],
1483
- swap: (first, second, third) => {
1484
- let type;
1485
- let data;
1486
- const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
1487
- if (typeof first === "function") {
1488
- const component = first;
1489
- let typeKey = componentRegistry.get(component);
1490
- if (!typeKey) {
1491
- warnInlineComponent(component, componentRegistry, warnedNames);
1492
- typeKey = getNextKey();
1493
- componentRegistry.set(component, typeKey);
1494
- componentMap.set(typeKey, component);
1495
- }
1496
- type = typeKey;
1497
- data = second ?? {};
1498
- } else {
1499
- type = first;
1500
- data = second ?? {};
1501
- }
1502
- set((state) => {
1503
- const top = state.stack.at(-1);
1504
- if (!top) return state;
1505
- const newStack = [...state.stack];
1506
- newStack[newStack.length - 1] = {
1507
- ariaLabel: ariaLabel ?? top.ariaLabel,
1508
- data,
1509
- id: top.id,
1510
- type
1511
- };
1512
- return { stack: newStack };
1513
- });
1514
- }
1515
- };
1516
- })
1776
+ store: createStore()(createStoreState({
1777
+ componentMap,
1778
+ componentRegistry,
1779
+ config,
1780
+ getNextKey,
1781
+ pruneRegistry,
1782
+ resolve,
1783
+ warnedNames
1784
+ }))
1517
1785
  };
1518
1786
  };
1519
1787
  //#endregion
@@ -1557,12 +1825,11 @@ const createStacksheet = (config) => {
1557
1825
  if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
1558
1826
  return ctx;
1559
1827
  };
1560
- const EMPTY_SHEETS = {};
1561
1828
  const providerValue = {
1562
1829
  config: resolved,
1563
1830
  store
1564
1831
  };
1565
- const StacksheetProvider = ({ sheets = EMPTY_SHEETS, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
1832
+ const StacksheetProvider = ({ sheets, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
1566
1833
  value: providerValue,
1567
1834
  children: [children, /* @__PURE__ */ jsx(Portal, {
1568
1835
  asChild: false,
@@ -1611,19 +1878,20 @@ const createStacksheet = (config) => {
1611
1878
  const SheetBack = ({ asChild, className, style, children }) => {
1612
1879
  const { back, isNested } = useSheetPanel();
1613
1880
  if (!isNested) return null;
1614
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1615
- "aria-label": children ? void 0 : "Back",
1616
- 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),
1881
+ const isAsChild = asChild === true;
1882
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1883
+ "aria-label": children === void 0 || children === null ? "Back" : void 0,
1884
+ className: joinClassNames(isAsChild ? 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),
1617
1885
  onClick: back,
1618
1886
  style,
1619
- type: asChild ? void 0 : "button",
1887
+ type: isAsChild ? void 0 : "button",
1620
1888
  children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
1621
1889
  });
1622
1890
  };
1623
1891
  //#endregion
1624
1892
  //#region src/SheetParts/sheet-body.tsx
1625
1893
  const SheetBody = ({ asChild, className, style, children }) => {
1626
- if (asChild) return /* @__PURE__ */ jsx(Slot, {
1894
+ if (asChild === true) return /* @__PURE__ */ jsx(Slot, {
1627
1895
  className: joinClassNames("relative min-h-0 flex-1", className),
1628
1896
  "data-stacksheet-no-drag": "",
1629
1897
  style,
@@ -1647,12 +1915,13 @@ const SheetBody = ({ asChild, className, style, children }) => {
1647
1915
  //#region src/SheetParts/sheet-close.tsx
1648
1916
  const SheetClose = ({ asChild, className, style, children }) => {
1649
1917
  const { close } = useSheetPanel();
1650
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1651
- "aria-label": children ? void 0 : "Close",
1652
- 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),
1918
+ const isAsChild = asChild === true;
1919
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1920
+ "aria-label": children === void 0 || children === null ? "Close" : void 0,
1921
+ className: joinClassNames(isAsChild ? 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),
1653
1922
  onClick: close,
1654
1923
  style,
1655
- type: asChild ? void 0 : "button",
1924
+ type: isAsChild ? void 0 : "button",
1656
1925
  children: children ?? /* @__PURE__ */ jsx(XIcon, {})
1657
1926
  });
1658
1927
  };
@@ -1661,7 +1930,7 @@ const SheetClose = ({ asChild, className, style, children }) => {
1661
1930
  const SheetDescription = ({ asChild, className, style, children }) => {
1662
1931
  const { panelId, registerDescription } = useSheetPanel();
1663
1932
  useEffect(() => registerDescription(), [registerDescription]);
1664
- return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
1933
+ return /* @__PURE__ */ jsx(asChild === true ? Slot : "p", {
1665
1934
  className,
1666
1935
  id: `${panelId}-desc`,
1667
1936
  style,
@@ -1671,8 +1940,9 @@ const SheetDescription = ({ asChild, className, style, children }) => {
1671
1940
  //#endregion
1672
1941
  //#region src/SheetParts/sheet-footer.tsx
1673
1942
  const SheetFooter = ({ asChild, className, style, children }) => {
1674
- return /* @__PURE__ */ jsx(asChild ? Slot : "footer", {
1675
- className: joinClassNames(asChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
1943
+ const isAsChild = asChild === true;
1944
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "footer", {
1945
+ className: joinClassNames(isAsChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
1676
1946
  style,
1677
1947
  children
1678
1948
  });
@@ -1682,13 +1952,14 @@ const SheetFooter = ({ asChild, className, style, children }) => {
1682
1952
  const SheetHandle = ({ asChild, className, style, children }) => {
1683
1953
  const { close, back, isNested } = useSheetPanel();
1684
1954
  const dismiss = isNested ? back : close;
1685
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1955
+ const isAsChild = asChild === true;
1956
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1686
1957
  "aria-label": "Dismiss",
1687
- className: joinClassNames(asChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
1958
+ className: joinClassNames(isAsChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
1688
1959
  "data-stacksheet-handle": "",
1689
1960
  onClick: dismiss,
1690
1961
  style,
1691
- type: asChild ? void 0 : "button",
1962
+ type: isAsChild ? void 0 : "button",
1692
1963
  children: children ?? /* @__PURE__ */ jsx("div", {
1693
1964
  "aria-hidden": "true",
1694
1965
  className: "h-1 w-9 rounded-sm bg-current/25"
@@ -1698,8 +1969,9 @@ const SheetHandle = ({ asChild, className, style, children }) => {
1698
1969
  //#endregion
1699
1970
  //#region src/SheetParts/sheet-header.tsx
1700
1971
  const SheetHeader = ({ asChild, className, style, children }) => {
1701
- return /* @__PURE__ */ jsx(asChild ? Slot : "header", {
1702
- className: joinClassNames(asChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
1972
+ const isAsChild = asChild === true;
1973
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "header", {
1974
+ className: joinClassNames(isAsChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
1703
1975
  style,
1704
1976
  children
1705
1977
  });
@@ -1707,9 +1979,11 @@ const SheetHeader = ({ asChild, className, style, children }) => {
1707
1979
  //#endregion
1708
1980
  //#region src/SheetParts/sheet-title.tsx
1709
1981
  const SheetTitle = ({ asChild, className, style, children }) => {
1710
- const { panelId } = useSheetPanel();
1711
- return /* @__PURE__ */ jsx(asChild ? Slot : "h2", {
1712
- className: joinClassNames(asChild ? void 0 : "font-semibold text-sm", className),
1982
+ const { panelId, registerTitle } = useSheetPanel();
1983
+ useEffect(() => registerTitle(), [registerTitle]);
1984
+ const isAsChild = asChild === true;
1985
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "h2", {
1986
+ className: joinClassNames(isAsChild ? void 0 : "font-semibold text-sm", className),
1713
1987
  id: `${panelId}-title`,
1714
1988
  style,
1715
1989
  children