@trackunit/react-drawer 2.6.72 → 2.6.73

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.
package/index.cjs.js CHANGED
@@ -308,7 +308,7 @@ const PANEL_FOCUSABLE_SELECTOR = [
308
308
  * ```
309
309
  * @param {DrawerProps} props - The props for the Drawer component
310
310
  */
311
- const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, }) => {
311
+ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }) => {
312
312
  const { isSm } = reactComponents.useViewportBreakpoints();
313
313
  const shouldUsePortal = !isSm || renderInPortal;
314
314
  const panelRef = react.useRef(null);
@@ -316,6 +316,10 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
316
316
  // the portal (or first paint) has attached the DOM node — refs alone do not.
317
317
  const [panelElement, setPanelElement] = react.useState(null);
318
318
  const isOpenRef = react.useRef(isOpen);
319
+ const onExitCompleteRef = react.useRef(onExitComplete);
320
+ // Guards against firing onExitComplete twice for the same close cycle if the real
321
+ // transitionend and the headless RAF fallback both resolve the same exit-complete.
322
+ const hasFiredExitCompleteRef = react.useRef(false);
319
323
  const [drawerState, dispatch] = react.useReducer(drawerAnimationReducer, {
320
324
  ...INITIAL_DRAWER_ANIMATION_STATE,
321
325
  // Mount off-screen when initially open so the enter slide still plays.
@@ -325,7 +329,22 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
325
329
  react.useEffect(() => {
326
330
  isOpenRef.current = isOpen;
327
331
  }, [isOpen]);
332
+ react.useEffect(() => {
333
+ onExitCompleteRef.current = onExitComplete;
334
+ }, [onExitComplete]);
335
+ const fireExitComplete = react.useCallback(() => {
336
+ if (hasFiredExitCompleteRef.current) {
337
+ return;
338
+ }
339
+ hasFiredExitCompleteRef.current = true;
340
+ onExitCompleteRef.current?.();
341
+ }, []);
328
342
  react.useLayoutEffect(() => {
343
+ if (!isOpen) {
344
+ // A new close cycle is starting — arm the guard so this cycle's genuine
345
+ // exit-complete (real transitionend or headless RAF fallback) can fire.
346
+ hasFiredExitCompleteRef.current = false;
347
+ }
329
348
  dispatch({ type: isOpen ? "open" : "close" });
330
349
  }, [isOpen]);
331
350
  // Defer the open flip until after paint. A single useLayoutEffect flip commits
@@ -360,10 +379,11 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
360
379
  const runningAnimations = typeof panelNode.getAnimations === "function" ? panelNode.getAnimations() : [];
361
380
  if (runningAnimations.length === 0) {
362
381
  dispatch({ type: "transitionEnd" });
382
+ fireExitComplete();
363
383
  }
364
384
  });
365
385
  return () => cancelAnimationFrame(rafId);
366
- }, [drawerState.mode, drawerState.shouldRender]);
386
+ }, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
367
387
  // If transitionend is suppressed while a CSS animation is reported as running
368
388
  // (Playwright/iframe), unmount after the motion duration so close cannot stick.
369
389
  react.useEffect(() => {
@@ -418,9 +438,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
418
438
  return;
419
439
  }
420
440
  if (transitionEvent.propertyName === "transform") {
441
+ // A transform transitionend while mode is still "closed" but isOpen has already
442
+ // flipped back to true is an interrupted exit (the reopen's openVisual is still
443
+ // behind its double-rAF), not an exit-complete. Same guard the headless rAF
444
+ // fallback above already applies.
445
+ if (drawerState.mode === "closed" && isOpenRef.current) {
446
+ return;
447
+ }
448
+ const isGenuineExitComplete = drawerState.mode === "closed";
421
449
  dispatch({ type: "transitionEnd" });
450
+ if (isGenuineExitComplete) {
451
+ fireExitComplete();
452
+ }
422
453
  }
423
- }, []);
454
+ }, [drawerState.mode, fireExitComplete]);
424
455
  if (!drawerState.shouldRender) {
425
456
  return null;
426
457
  }
@@ -490,6 +521,48 @@ const useDrawer = (props) => {
490
521
  }), [dismissProp]);
491
522
  const [internalIsOpen, setIsOpen] = react.useState(defaultOpen ?? false);
492
523
  const isOpen = typeof controlledIsOpen === "boolean" ? controlledIsOpen : internalIsOpen;
524
+ // `false` initially / whenever never opened. Flips to `true` the moment the
525
+ // resolved `isOpen` value above transitions `true -> false`, and back to
526
+ // `false` either when it transitions `false -> true` again or once Drawer's
527
+ // `onExitComplete` signals the panel has genuinely finished exiting
528
+ // (`handleExitComplete`). See `UseDrawerReturnValue.isExiting`.
529
+ const [isExiting, setIsExiting] = react.useState(false);
530
+ // Tracks the resolved `isOpen` value as of the last render for which
531
+ // `isExiting` was derived below, so the render-phase check can detect a
532
+ // transition without an effect.
533
+ const [prevIsOpen, setPrevIsOpen] = react.useState(isOpen);
534
+ // Derive `isExiting` from the resolved `isOpen` value's own transitions —
535
+ // the same value used everywhere else in this hook — rather than only from
536
+ // the imperative `close()` / `open()` call sites below. This is what covers
537
+ // a controlled consumer flipping the `isOpen` prop directly (the documented
538
+ // `useDrawer({ isOpen: Boolean(selectedId) })` pattern): `close()` / `open()`
539
+ // alone only ever observe imperative calls, not a controlled prop changing
540
+ // for some other reason (e.g. selecting a different row).
541
+ //
542
+ // This calls `setState` conditionally during the render body — React's
543
+ // "adjust state during render" pattern
544
+ // (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
545
+ // — rather than a passive `useEffect` (e.g. `useWatch`). A passive effect
546
+ // only runs one commit AFTER the render where `isOpen` changes, leaving an
547
+ // intermediate commit where `isOpen` has already changed but `isExiting`
548
+ // hasn't yet. `useHold` — `isExiting`'s primary documented consumer — reads
549
+ // both in its own effect and would observe that stale `isExiting: false`
550
+ // there, permanently overwriting its held ref with the already-changed
551
+ // value before `isExiting` ever flips `true`. Calling `setState` here
552
+ // instead makes React detect the change and re-render immediately, before
553
+ // committing or running any effects, so `isOpen` and `isExiting` always
554
+ // land together in the same effective commit that `useHold` observes.
555
+ //
556
+ // `handleExitComplete` (below) is a second, separate way `isExiting` flips
557
+ // back to `false` — the normal close -> fully-exited path. It never fights
558
+ // with this derivation: by the time it fires, `isOpen` has already settled
559
+ // to `false` and `prevIsOpen` has already caught up to it (from the render
560
+ // that flipped `isExiting` to `true` in the first place), so `isOpen ===
561
+ // prevIsOpen` and this block is a no-op on the next render either way.
562
+ if (isOpen !== prevIsOpen) {
563
+ setPrevIsOpen(isOpen);
564
+ setIsExiting(!isOpen); // true when transitioning to closed, false when transitioning to open
565
+ }
493
566
  const isPendingCloseRef = react.useRef(false);
494
567
  const onCloseRef = react.useRef(onClose);
495
568
  const onOpenRef = react.useRef(onOpen);
@@ -506,6 +579,11 @@ const useDrawer = (props) => {
506
579
  onCloseRef.current?.(event, reason);
507
580
  onOpenChangeRef.current?.(false, event, reason);
508
581
  }, []);
582
+ // Fired by `Drawer`'s internal `onExitComplete` prop exactly once per genuine
583
+ // close→fully-exited cycle — the normal path back to `isExiting: false`.
584
+ const handleExitComplete = react.useCallback(() => {
585
+ setIsExiting(false);
586
+ }, []);
509
587
  const requestClose = react.useCallback((event, reason) => {
510
588
  if (onBeforeCloseRef.current) {
511
589
  if (isPendingCloseRef.current) {
@@ -530,6 +608,12 @@ const useDrawer = (props) => {
530
608
  onOpenRef.current?.();
531
609
  onOpenChangeRef.current?.(true);
532
610
  setIsOpen(true);
611
+ // `isExiting` releases automatically here via the render-phase derivation
612
+ // above (the resolved `isOpen` transitioning back to `true`) — a reopen
613
+ // before the exit signal fires releases any hold right away, since the
614
+ // interrupted exit's transitionend resolves as an enter-settle, so
615
+ // `onExitComplete` correctly never fires for that cycle. See
616
+ // `UseDrawerReturnValue.isExiting`.
533
617
  }, []);
534
618
  const toggle = react.useCallback(() => {
535
619
  if (isOpen) {
@@ -564,7 +648,21 @@ const useDrawer = (props) => {
564
648
  trapFocus,
565
649
  position,
566
650
  floatingUi,
567
- }), [isOpen, open, close, toggle, requestClose, variant, trapFocus, position, floatingUi]);
651
+ isExiting,
652
+ onExitComplete: handleExitComplete,
653
+ }), [
654
+ isOpen,
655
+ open,
656
+ close,
657
+ toggle,
658
+ requestClose,
659
+ variant,
660
+ trapFocus,
661
+ position,
662
+ floatingUi,
663
+ isExiting,
664
+ handleExitComplete,
665
+ ]);
568
666
  };
569
667
 
570
668
  /**
package/index.esm.js CHANGED
@@ -2,7 +2,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
3
3
  import { useMergeRefs, FloatingFocusManager, useFloating, useDismiss, useInteractions } from '@floating-ui/react';
4
4
  import { useViewportBreakpoints, Portal, IconButton, Icon, MoreMenu } from '@trackunit/react-components';
5
- import { useRef, useState, useReducer, useEffect, useLayoutEffect, useCallback, useMemo } from 'react';
5
+ import { useRef, useState, useReducer, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
6
6
  import { cva } from '@trackunit/css-class-variance-utilities';
7
7
  import { twMerge } from 'tailwind-merge';
8
8
 
@@ -306,7 +306,7 @@ const PANEL_FOCUSABLE_SELECTOR = [
306
306
  * ```
307
307
  * @param {DrawerProps} props - The props for the Drawer component
308
308
  */
309
- const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, }) => {
309
+ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }) => {
310
310
  const { isSm } = useViewportBreakpoints();
311
311
  const shouldUsePortal = !isSm || renderInPortal;
312
312
  const panelRef = useRef(null);
@@ -314,6 +314,10 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
314
314
  // the portal (or first paint) has attached the DOM node — refs alone do not.
315
315
  const [panelElement, setPanelElement] = useState(null);
316
316
  const isOpenRef = useRef(isOpen);
317
+ const onExitCompleteRef = useRef(onExitComplete);
318
+ // Guards against firing onExitComplete twice for the same close cycle if the real
319
+ // transitionend and the headless RAF fallback both resolve the same exit-complete.
320
+ const hasFiredExitCompleteRef = useRef(false);
317
321
  const [drawerState, dispatch] = useReducer(drawerAnimationReducer, {
318
322
  ...INITIAL_DRAWER_ANIMATION_STATE,
319
323
  // Mount off-screen when initially open so the enter slide still plays.
@@ -323,7 +327,22 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
323
327
  useEffect(() => {
324
328
  isOpenRef.current = isOpen;
325
329
  }, [isOpen]);
330
+ useEffect(() => {
331
+ onExitCompleteRef.current = onExitComplete;
332
+ }, [onExitComplete]);
333
+ const fireExitComplete = useCallback(() => {
334
+ if (hasFiredExitCompleteRef.current) {
335
+ return;
336
+ }
337
+ hasFiredExitCompleteRef.current = true;
338
+ onExitCompleteRef.current?.();
339
+ }, []);
326
340
  useLayoutEffect(() => {
341
+ if (!isOpen) {
342
+ // A new close cycle is starting — arm the guard so this cycle's genuine
343
+ // exit-complete (real transitionend or headless RAF fallback) can fire.
344
+ hasFiredExitCompleteRef.current = false;
345
+ }
327
346
  dispatch({ type: isOpen ? "open" : "close" });
328
347
  }, [isOpen]);
329
348
  // Defer the open flip until after paint. A single useLayoutEffect flip commits
@@ -358,10 +377,11 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
358
377
  const runningAnimations = typeof panelNode.getAnimations === "function" ? panelNode.getAnimations() : [];
359
378
  if (runningAnimations.length === 0) {
360
379
  dispatch({ type: "transitionEnd" });
380
+ fireExitComplete();
361
381
  }
362
382
  });
363
383
  return () => cancelAnimationFrame(rafId);
364
- }, [drawerState.mode, drawerState.shouldRender]);
384
+ }, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
365
385
  // If transitionend is suppressed while a CSS animation is reported as running
366
386
  // (Playwright/iframe), unmount after the motion duration so close cannot stick.
367
387
  useEffect(() => {
@@ -416,9 +436,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
416
436
  return;
417
437
  }
418
438
  if (transitionEvent.propertyName === "transform") {
439
+ // A transform transitionend while mode is still "closed" but isOpen has already
440
+ // flipped back to true is an interrupted exit (the reopen's openVisual is still
441
+ // behind its double-rAF), not an exit-complete. Same guard the headless rAF
442
+ // fallback above already applies.
443
+ if (drawerState.mode === "closed" && isOpenRef.current) {
444
+ return;
445
+ }
446
+ const isGenuineExitComplete = drawerState.mode === "closed";
419
447
  dispatch({ type: "transitionEnd" });
448
+ if (isGenuineExitComplete) {
449
+ fireExitComplete();
450
+ }
420
451
  }
421
- }, []);
452
+ }, [drawerState.mode, fireExitComplete]);
422
453
  if (!drawerState.shouldRender) {
423
454
  return null;
424
455
  }
@@ -488,6 +519,48 @@ const useDrawer = (props) => {
488
519
  }), [dismissProp]);
489
520
  const [internalIsOpen, setIsOpen] = useState(defaultOpen ?? false);
490
521
  const isOpen = typeof controlledIsOpen === "boolean" ? controlledIsOpen : internalIsOpen;
522
+ // `false` initially / whenever never opened. Flips to `true` the moment the
523
+ // resolved `isOpen` value above transitions `true -> false`, and back to
524
+ // `false` either when it transitions `false -> true` again or once Drawer's
525
+ // `onExitComplete` signals the panel has genuinely finished exiting
526
+ // (`handleExitComplete`). See `UseDrawerReturnValue.isExiting`.
527
+ const [isExiting, setIsExiting] = useState(false);
528
+ // Tracks the resolved `isOpen` value as of the last render for which
529
+ // `isExiting` was derived below, so the render-phase check can detect a
530
+ // transition without an effect.
531
+ const [prevIsOpen, setPrevIsOpen] = useState(isOpen);
532
+ // Derive `isExiting` from the resolved `isOpen` value's own transitions —
533
+ // the same value used everywhere else in this hook — rather than only from
534
+ // the imperative `close()` / `open()` call sites below. This is what covers
535
+ // a controlled consumer flipping the `isOpen` prop directly (the documented
536
+ // `useDrawer({ isOpen: Boolean(selectedId) })` pattern): `close()` / `open()`
537
+ // alone only ever observe imperative calls, not a controlled prop changing
538
+ // for some other reason (e.g. selecting a different row).
539
+ //
540
+ // This calls `setState` conditionally during the render body — React's
541
+ // "adjust state during render" pattern
542
+ // (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
543
+ // — rather than a passive `useEffect` (e.g. `useWatch`). A passive effect
544
+ // only runs one commit AFTER the render where `isOpen` changes, leaving an
545
+ // intermediate commit where `isOpen` has already changed but `isExiting`
546
+ // hasn't yet. `useHold` — `isExiting`'s primary documented consumer — reads
547
+ // both in its own effect and would observe that stale `isExiting: false`
548
+ // there, permanently overwriting its held ref with the already-changed
549
+ // value before `isExiting` ever flips `true`. Calling `setState` here
550
+ // instead makes React detect the change and re-render immediately, before
551
+ // committing or running any effects, so `isOpen` and `isExiting` always
552
+ // land together in the same effective commit that `useHold` observes.
553
+ //
554
+ // `handleExitComplete` (below) is a second, separate way `isExiting` flips
555
+ // back to `false` — the normal close -> fully-exited path. It never fights
556
+ // with this derivation: by the time it fires, `isOpen` has already settled
557
+ // to `false` and `prevIsOpen` has already caught up to it (from the render
558
+ // that flipped `isExiting` to `true` in the first place), so `isOpen ===
559
+ // prevIsOpen` and this block is a no-op on the next render either way.
560
+ if (isOpen !== prevIsOpen) {
561
+ setPrevIsOpen(isOpen);
562
+ setIsExiting(!isOpen); // true when transitioning to closed, false when transitioning to open
563
+ }
491
564
  const isPendingCloseRef = useRef(false);
492
565
  const onCloseRef = useRef(onClose);
493
566
  const onOpenRef = useRef(onOpen);
@@ -504,6 +577,11 @@ const useDrawer = (props) => {
504
577
  onCloseRef.current?.(event, reason);
505
578
  onOpenChangeRef.current?.(false, event, reason);
506
579
  }, []);
580
+ // Fired by `Drawer`'s internal `onExitComplete` prop exactly once per genuine
581
+ // close→fully-exited cycle — the normal path back to `isExiting: false`.
582
+ const handleExitComplete = useCallback(() => {
583
+ setIsExiting(false);
584
+ }, []);
507
585
  const requestClose = useCallback((event, reason) => {
508
586
  if (onBeforeCloseRef.current) {
509
587
  if (isPendingCloseRef.current) {
@@ -528,6 +606,12 @@ const useDrawer = (props) => {
528
606
  onOpenRef.current?.();
529
607
  onOpenChangeRef.current?.(true);
530
608
  setIsOpen(true);
609
+ // `isExiting` releases automatically here via the render-phase derivation
610
+ // above (the resolved `isOpen` transitioning back to `true`) — a reopen
611
+ // before the exit signal fires releases any hold right away, since the
612
+ // interrupted exit's transitionend resolves as an enter-settle, so
613
+ // `onExitComplete` correctly never fires for that cycle. See
614
+ // `UseDrawerReturnValue.isExiting`.
531
615
  }, []);
532
616
  const toggle = useCallback(() => {
533
617
  if (isOpen) {
@@ -562,7 +646,21 @@ const useDrawer = (props) => {
562
646
  trapFocus,
563
647
  position,
564
648
  floatingUi,
565
- }), [isOpen, open, close, toggle, requestClose, variant, trapFocus, position, floatingUi]);
649
+ isExiting,
650
+ onExitComplete: handleExitComplete,
651
+ }), [
652
+ isOpen,
653
+ open,
654
+ close,
655
+ toggle,
656
+ requestClose,
657
+ variant,
658
+ trapFocus,
659
+ position,
660
+ floatingUi,
661
+ isExiting,
662
+ handleExitComplete,
663
+ ]);
566
664
  };
567
665
 
568
666
  /**
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@trackunit/react-drawer",
3
- "version": "2.6.72",
3
+ "version": "2.6.73",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/react-components": "2.13.24",
10
+ "@trackunit/react-components": "2.13.25",
11
11
  "@trackunit/css-class-variance-utilities": "2.0.14",
12
12
  "@trackunit/i18n-library-translation": "2.4.63",
13
13
  "@floating-ui/react": "^0.26.25",
@@ -73,6 +73,17 @@ export interface DrawerProps extends CommonProps {
73
73
  * the drawer body renders a visible heading so the two stay in sync.
74
74
  */
75
75
  ariaLabelledBy?: string;
76
+ /**
77
+ * Internal signal fired exactly once per genuine open→close→fully-exited cycle,
78
+ * once the panel has actually finished animating (or would have, in headless /
79
+ * test environments where CSS transitions are suppressed) off-screen. Spread from
80
+ * `useDrawer()`, which uses it to flip its `isExiting` state back to `false`.
81
+ *
82
+ * Does not fire for the enter-settle transitionend, on initial mount, while
83
+ * remaining closed, or more than once for the same close cycle even if both the
84
+ * real `transitionend` and the headless RAF fallback resolve it.
85
+ */
86
+ readonly onExitComplete?: () => void;
76
87
  }
77
88
  /**
78
89
  * Drawers slide in from the left or right edge of the viewport as either a modal
@@ -135,6 +146,6 @@ export interface DrawerProps extends CommonProps {
135
146
  * @param {DrawerProps} props - The props for the Drawer component
136
147
  */
137
148
  export declare const Drawer: {
138
- ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal, containerClassName, ariaLabel, ariaLabelledBy, }: DrawerProps): ReactElement | null;
149
+ ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }: DrawerProps): ReactElement | null;
139
150
  displayName: string;
140
151
  };
@@ -98,6 +98,33 @@ export type UseDrawerReturnValue = {
98
98
  * its `FloatingFocusManager`.
99
99
  */
100
100
  readonly floatingUi: DrawerFloatingUiProps;
101
+ /**
102
+ * Whether the drawer is currently in its close→fully-exited cycle. `false`
103
+ * initially and whenever the drawer has never been opened.
104
+ *
105
+ * Derived from the resolved `isOpen` value's own transitions (the same value
106
+ * used everywhere else in the hook — `controlledIsOpen ?? internalIsOpen`),
107
+ * not merely from the imperative `open()` / `close()` call sites. Flips to
108
+ * `true` the moment `isOpen` transitions `true -> false` — whether via
109
+ * `close()` / `requestClose()` committing (after any `onBeforeClose` guard
110
+ * has resolved `true`) or a controlled consumer flipping its `isOpen` prop
111
+ * directly — and back to `false` the moment `isOpen` transitions
112
+ * `false -> true` again (an interrupted exit releases any hold immediately,
113
+ * since the interrupted exit's `transitionend` resolves as an enter-settle
114
+ * and never signals exit-complete), or when the panel has genuinely finished
115
+ * animating off-screen (wired internally via `onExitComplete`, below).
116
+ *
117
+ * Useful for content that wants to survive the exit animation — e.g. via the
118
+ * shared `useHold(value, isExiting)` primitive — by holding its last non-null
119
+ * value while `isExiting` is `true`.
120
+ */
121
+ readonly isExiting: boolean;
122
+ /**
123
+ * Internal signal spread onto `Drawer`'s `onExitComplete` prop. `Drawer` calls
124
+ * this exactly once per genuine close→fully-exited cycle, which flips
125
+ * `isExiting` back to `false`. Not intended for direct use by consumers.
126
+ */
127
+ readonly onExitComplete: () => void;
101
128
  };
102
129
  /**
103
130
  * Hook for managing Drawer open/close state, dismiss handling, and floating UI wiring.