@seed-design/react-dismissible-layer 0.0.0-alpha-20260414104312

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.
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { composeRefs } from '@radix-ui/react-compose-refs';
4
+ import { Slot } from '@radix-ui/react-slot';
5
+ import { forwardRef } from 'react';
6
+ import { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
7
+ import { D as DismissibleParentContext } from './layer-stack-12s-DjRVp1_u.js';
8
+
9
+ const DismissibleLayer = /*#__PURE__*/ forwardRef((props, ref)=>{
10
+ const { children, ...options } = props;
11
+ const { dismissibleRef, dismissibleProps, layerNode } = useDismissibleLayer(options);
12
+ return /*#__PURE__*/ jsx(DismissibleParentContext.Provider, {
13
+ value: layerNode,
14
+ children: /*#__PURE__*/ jsx(Slot, {
15
+ ref: composeRefs(ref, dismissibleRef),
16
+ ...dismissibleProps,
17
+ children: children
18
+ })
19
+ });
20
+ });
21
+ DismissibleLayer.displayName = "DismissibleLayer";
22
+
23
+ export { DismissibleLayer as D };
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ var jsxRuntime = require('react/jsx-runtime');
3
+ var reactComposeRefs = require('@radix-ui/react-compose-refs');
4
+ var reactSlot = require('@radix-ui/react-slot');
5
+ var react = require('react');
6
+ var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
7
+ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
8
+
9
+ const DismissibleLayer = /*#__PURE__*/ react.forwardRef((props, ref)=>{
10
+ const { children, ...options } = props;
11
+ const { dismissibleRef, dismissibleProps, layerNode } = useDismissibleLayer12s.useDismissibleLayer(options);
12
+ return /*#__PURE__*/ jsxRuntime.jsx(layerStack12s.DismissibleParentContext.Provider, {
13
+ value: layerNode,
14
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactSlot.Slot, {
15
+ ref: reactComposeRefs.composeRefs(ref, dismissibleRef),
16
+ ...dismissibleProps,
17
+ children: children
18
+ })
19
+ });
20
+ });
21
+ DismissibleLayer.displayName = "DismissibleLayer";
22
+
23
+ exports.DismissibleLayer = DismissibleLayer;
package/lib/index.cjs ADDED
@@ -0,0 +1,13 @@
1
+ var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
2
+ var DismissibleLayer12s = require('./DismissibleLayer-12s-ZXi0vrlt.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
4
+
5
+
6
+
7
+ exports.useDismissibleLayer = useDismissibleLayer12s.useDismissibleLayer;
8
+ exports.DismissibleLayer = DismissibleLayer12s.DismissibleLayer;
9
+ exports.LayerStackContext = layerStack12s.LayerStackContext;
10
+ exports.addBranch = layerStack12s.addBranch;
11
+ exports.isTopMost = layerStack12s.isTopMost;
12
+ exports.removeBranch = layerStack12s.removeBranch;
13
+ exports.useLayerStackContext = layerStack12s.useLayerStackContext;
package/lib/index.d.ts ADDED
@@ -0,0 +1,112 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties } from 'react';
3
+
4
+ interface CascadeDismissDetail {
5
+ /** The layer node that was removed, causing this cascade dismiss. */
6
+ dismissedParent: HTMLElement;
7
+ }
8
+ interface Layer {
9
+ node: HTMLElement;
10
+ dismiss: (detail: CascadeDismissDetail) => void;
11
+ blockPointerEvents?: boolean;
12
+ /**
13
+ * The parent layer's node. Used to determine cascade-dismiss scope.
14
+ * Only layers whose parentNode matches the removed layer's node will be
15
+ * cascade-dismissed. Layers without a parentNode are top-level and never
16
+ * cascade-dismissed by sibling layer removal.
17
+ */
18
+ parentNode?: HTMLElement | null;
19
+ }
20
+ interface LayerStackContextValue {
21
+ layers: Layer[];
22
+ branches: HTMLElement[];
23
+ recentlyRemoved: Set<HTMLElement>;
24
+ }
25
+ /**
26
+ * Shared layer stack context. No Provider needed — all consumers share the same
27
+ * default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
28
+ * pattern, with the option to scope via Provider if needed.
29
+ */
30
+ declare const LayerStackContext: react.Context<LayerStackContextValue>;
31
+ declare function useLayerStackContext(): LayerStackContextValue;
32
+ declare function isTopMost(ctx: LayerStackContextValue, node: HTMLElement): boolean;
33
+ declare function addBranch(ctx: LayerStackContextValue, node: HTMLElement): void;
34
+ declare function removeBranch(ctx: LayerStackContextValue, node: HTMLElement): void;
35
+
36
+ interface UseDismissibleLayerOptions {
37
+ /**
38
+ * Whether the dismissible layer is active. When false, the layer is not
39
+ * registered in the stack and no event listeners are attached.
40
+ */
41
+ enabled: boolean;
42
+ /**
43
+ * When true, disables pointer events on elements outside this layer.
44
+ * Used for modal overlays (Dialog, BottomSheet).
45
+ */
46
+ blockPointerEvents?: boolean;
47
+ /**
48
+ * Called when escape key is pressed while this layer is topmost.
49
+ * Call `event.preventDefault()` to prevent dismiss.
50
+ */
51
+ onEscapeKeyDown: (event: KeyboardEvent) => void;
52
+ /**
53
+ * Called when a press occurs outside the layer.
54
+ * Call `event.preventDefault()` to signal that the event is handled.
55
+ */
56
+ onPressOutside: (event: PointerEvent | TouchEvent) => void;
57
+ /**
58
+ * Called when focus moves outside the layer.
59
+ * Call `event.preventDefault()` to signal that the event is handled.
60
+ */
61
+ onFocusOutside: (event: FocusEvent) => void;
62
+ /**
63
+ * Called when a parent layer is removed and this layer should close as a consequence.
64
+ * Unlike escape/outside/focus callbacks, this is not preventable.
65
+ */
66
+ onCascadeDismiss: (detail: CascadeDismissDetail) => void;
67
+ /**
68
+ * Custom function to determine if a target should be treated as "inside".
69
+ * Useful for trigger elements that are outside the layer DOM but should
70
+ * not trigger dismiss (e.g., Menu trigger).
71
+ */
72
+ exclude?: (target: HTMLElement) => boolean;
73
+ /**
74
+ * Determines when an outside press triggers dismiss.
75
+ *
76
+ * - `"confirm"` (default): Both mouse and touch defer to click.
77
+ * - `"eager"`: Mouse on pointerdown, touch defers to click.
78
+ * - `"drag"`: Mouse on pointerdown, touch on drag (>10px immediate, >5px on touchend).
79
+ */
80
+ pressBehavior?: "eager" | "confirm" | "drag";
81
+ }
82
+ declare function useDismissibleLayer(options: UseDismissibleLayerOptions): {
83
+ dismissibleRef: (el: HTMLElement | null) => void;
84
+ dismissibleProps: {
85
+ onPointerDownCapture: () => void;
86
+ onFocusCapture: () => void;
87
+ onBlurCapture: () => void;
88
+ style?: undefined;
89
+ };
90
+ isTopLayer: boolean;
91
+ /** The current layer node, for providing DismissibleParentContext to children. */
92
+ layerNode: HTMLElement | null;
93
+ } | {
94
+ dismissibleRef: (el: HTMLElement | null) => void;
95
+ dismissibleProps: {
96
+ onPointerDownCapture: () => void;
97
+ onFocusCapture: () => void;
98
+ onBlurCapture: () => void;
99
+ style: CSSProperties;
100
+ };
101
+ isTopLayer: boolean;
102
+ /** The current layer node, for providing DismissibleParentContext to children. */
103
+ layerNode: HTMLElement | null;
104
+ };
105
+
106
+ interface DismissibleLayerProps extends UseDismissibleLayerOptions {
107
+ children: React.ReactNode;
108
+ }
109
+ declare const DismissibleLayer: react.ForwardRefExoticComponent<DismissibleLayerProps & react.RefAttributes<HTMLElement>>;
110
+
111
+ export { DismissibleLayer, LayerStackContext, addBranch, isTopMost, removeBranch, useDismissibleLayer, useLayerStackContext };
112
+ export type { CascadeDismissDetail, DismissibleLayerProps, Layer, LayerStackContextValue, UseDismissibleLayerOptions };
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
2
+ export { D as DismissibleLayer } from './DismissibleLayer-12s-Cz1jvGI2.js';
3
+ export { f as LayerStackContext, h as addBranch, i as isTopMost, j as removeBranch, u as useLayerStackContext } from './layer-stack-12s-DjRVp1_u.js';
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+ var react = require('react');
3
+
4
+ const LAYER_UPDATE_EVENT = "seed:dismissible-update";
5
+ /**
6
+ * Shared layer stack context. No Provider needed — all consumers share the same
7
+ * default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
8
+ * pattern, with the option to scope via Provider if needed.
9
+ */ const LayerStackContext = react.createContext({
10
+ layers: [],
11
+ branches: [],
12
+ recentlyRemoved: new Set()
13
+ });
14
+ function useLayerStackContext() {
15
+ return react.useContext(LayerStackContext);
16
+ }
17
+ function isTopMost(ctx, node) {
18
+ return ctx.layers.at(-1)?.node === node;
19
+ }
20
+ function contains(parent, child) {
21
+ if (!parent || !child) return false;
22
+ if (!(child instanceof Node)) return false;
23
+ return parent.contains(child);
24
+ }
25
+ function isInNestedLayer(ctx, node, target) {
26
+ const index = ctx.layers.findIndex((l)=>l.node === node);
27
+ if (index === -1) return false;
28
+ const nested = ctx.layers.slice(index + 1);
29
+ if (nested.some((layer)=>contains(layer.node, target))) return true;
30
+ // During layer removal, treat all focus events as "inside" to prevent
31
+ // cascading dismissals from focus transitions (Zag pattern).
32
+ if (ctx.recentlyRemoved.size > 0) return true;
33
+ return false;
34
+ }
35
+ function isInBranch(ctx, target) {
36
+ return ctx.branches.some((branch)=>contains(branch, target));
37
+ }
38
+ function isBelowPointerBlockingLayer(ctx, node) {
39
+ const index = ctx.layers.findIndex((l)=>l.node === node);
40
+ const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
41
+ const highestBlocking = blockingLayers.at(-1);
42
+ if (!highestBlocking) return false;
43
+ const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
44
+ return index < highestBlockingIndex;
45
+ }
46
+ function getPointerEventsEnabled(ctx, node) {
47
+ const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
48
+ if (!hasBlocking) return true;
49
+ const index = ctx.layers.findIndex((l)=>l.node === node);
50
+ const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
51
+ const highestBlocking = blockingLayers.at(-1);
52
+ if (!highestBlocking) return true;
53
+ const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
54
+ return index >= highestBlockingIndex;
55
+ }
56
+ function notifyLayerChange() {
57
+ document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
58
+ }
59
+ function addLayer(ctx, layer) {
60
+ // Guard against double-registration (e.g., React strict mode re-running effects).
61
+ const existing = ctx.layers.findIndex((l)=>l.node === layer.node);
62
+ if (existing >= 0) ctx.layers.splice(existing, 1);
63
+ ctx.layers.push(layer);
64
+ notifyLayerChange();
65
+ }
66
+ function removeLayer(ctx, node) {
67
+ const index = ctx.layers.findIndex((l)=>l.node === node);
68
+ if (index < 0) return;
69
+ // Track recently removed to handle focus race conditions during cleanup.
70
+ // This prevents parent layers from incorrectly dismissing when focus
71
+ // moves from a closing nested layer.
72
+ ctx.recentlyRemoved.add(node);
73
+ queueMicrotask(()=>ctx.recentlyRemoved.delete(node));
74
+ // Cascade-dismiss only layers that declare this node as their parent.
75
+ // This prevents sibling layers (independent menus, dialogs, etc.) from
76
+ // being incorrectly dismissed when an unrelated layer closes.
77
+ // Transitive children are handled by their own removeLayer calls.
78
+ const children = ctx.layers.filter((l)=>l.parentNode === node);
79
+ for (const child of children){
80
+ child.dismiss({
81
+ dismissedParent: node
82
+ });
83
+ }
84
+ // Re-compute index: cascade-dismiss may have recursively removed layers,
85
+ // shifting the target's position in the array. In React, parents mount
86
+ // before children so children are always at higher indices — but this is
87
+ // a pure data structure with no React dependency, so we defend against
88
+ // arbitrary insertion order.
89
+ const freshIndex = ctx.layers.findIndex((l)=>l.node === node);
90
+ if (freshIndex >= 0) ctx.layers.splice(freshIndex, 1);
91
+ notifyLayerChange();
92
+ }
93
+ /**
94
+ * React context that propagates a parent dismissible layer's node down the
95
+ * React tree (including through portals). Nested dismissible layers read this
96
+ * to register as children of the parent, enabling correct cascade-dismiss
97
+ * behavior without affecting unrelated sibling layers.
98
+ *
99
+ * Follows the same pattern as base-ui's FloatingNodeContext.
100
+ */ const DismissibleParentContext = react.createContext(null);
101
+ function useDismissibleParentNode() {
102
+ return react.useContext(DismissibleParentContext);
103
+ }
104
+ function addBranch(ctx, node) {
105
+ ctx.branches.push(node);
106
+ }
107
+ function removeBranch(ctx, node) {
108
+ const index = ctx.branches.indexOf(node);
109
+ if (index >= 0) ctx.branches.splice(index, 1);
110
+ }
111
+
112
+ exports.DismissibleParentContext = DismissibleParentContext;
113
+ exports.LAYER_UPDATE_EVENT = LAYER_UPDATE_EVENT;
114
+ exports.LayerStackContext = LayerStackContext;
115
+ exports.addBranch = addBranch;
116
+ exports.addLayer = addLayer;
117
+ exports.getPointerEventsEnabled = getPointerEventsEnabled;
118
+ exports.isBelowPointerBlockingLayer = isBelowPointerBlockingLayer;
119
+ exports.isInBranch = isInBranch;
120
+ exports.isInNestedLayer = isInNestedLayer;
121
+ exports.isTopMost = isTopMost;
122
+ exports.removeBranch = removeBranch;
123
+ exports.removeLayer = removeLayer;
124
+ exports.useDismissibleParentNode = useDismissibleParentNode;
125
+ exports.useLayerStackContext = useLayerStackContext;
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ const LAYER_UPDATE_EVENT = "seed:dismissible-update";
5
+ /**
6
+ * Shared layer stack context. No Provider needed — all consumers share the same
7
+ * default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
8
+ * pattern, with the option to scope via Provider if needed.
9
+ */ const LayerStackContext = createContext({
10
+ layers: [],
11
+ branches: [],
12
+ recentlyRemoved: new Set()
13
+ });
14
+ function useLayerStackContext() {
15
+ return useContext(LayerStackContext);
16
+ }
17
+ function isTopMost(ctx, node) {
18
+ return ctx.layers.at(-1)?.node === node;
19
+ }
20
+ function contains(parent, child) {
21
+ if (!parent || !child) return false;
22
+ if (!(child instanceof Node)) return false;
23
+ return parent.contains(child);
24
+ }
25
+ function isInNestedLayer(ctx, node, target) {
26
+ const index = ctx.layers.findIndex((l)=>l.node === node);
27
+ if (index === -1) return false;
28
+ const nested = ctx.layers.slice(index + 1);
29
+ if (nested.some((layer)=>contains(layer.node, target))) return true;
30
+ // During layer removal, treat all focus events as "inside" to prevent
31
+ // cascading dismissals from focus transitions (Zag pattern).
32
+ if (ctx.recentlyRemoved.size > 0) return true;
33
+ return false;
34
+ }
35
+ function isInBranch(ctx, target) {
36
+ return ctx.branches.some((branch)=>contains(branch, target));
37
+ }
38
+ function isBelowPointerBlockingLayer(ctx, node) {
39
+ const index = ctx.layers.findIndex((l)=>l.node === node);
40
+ const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
41
+ const highestBlocking = blockingLayers.at(-1);
42
+ if (!highestBlocking) return false;
43
+ const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
44
+ return index < highestBlockingIndex;
45
+ }
46
+ function getPointerEventsEnabled(ctx, node) {
47
+ const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
48
+ if (!hasBlocking) return true;
49
+ const index = ctx.layers.findIndex((l)=>l.node === node);
50
+ const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
51
+ const highestBlocking = blockingLayers.at(-1);
52
+ if (!highestBlocking) return true;
53
+ const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
54
+ return index >= highestBlockingIndex;
55
+ }
56
+ function notifyLayerChange() {
57
+ document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
58
+ }
59
+ function addLayer(ctx, layer) {
60
+ // Guard against double-registration (e.g., React strict mode re-running effects).
61
+ const existing = ctx.layers.findIndex((l)=>l.node === layer.node);
62
+ if (existing >= 0) ctx.layers.splice(existing, 1);
63
+ ctx.layers.push(layer);
64
+ notifyLayerChange();
65
+ }
66
+ function removeLayer(ctx, node) {
67
+ const index = ctx.layers.findIndex((l)=>l.node === node);
68
+ if (index < 0) return;
69
+ // Track recently removed to handle focus race conditions during cleanup.
70
+ // This prevents parent layers from incorrectly dismissing when focus
71
+ // moves from a closing nested layer.
72
+ ctx.recentlyRemoved.add(node);
73
+ queueMicrotask(()=>ctx.recentlyRemoved.delete(node));
74
+ // Cascade-dismiss only layers that declare this node as their parent.
75
+ // This prevents sibling layers (independent menus, dialogs, etc.) from
76
+ // being incorrectly dismissed when an unrelated layer closes.
77
+ // Transitive children are handled by their own removeLayer calls.
78
+ const children = ctx.layers.filter((l)=>l.parentNode === node);
79
+ for (const child of children){
80
+ child.dismiss({
81
+ dismissedParent: node
82
+ });
83
+ }
84
+ // Re-compute index: cascade-dismiss may have recursively removed layers,
85
+ // shifting the target's position in the array. In React, parents mount
86
+ // before children so children are always at higher indices — but this is
87
+ // a pure data structure with no React dependency, so we defend against
88
+ // arbitrary insertion order.
89
+ const freshIndex = ctx.layers.findIndex((l)=>l.node === node);
90
+ if (freshIndex >= 0) ctx.layers.splice(freshIndex, 1);
91
+ notifyLayerChange();
92
+ }
93
+ /**
94
+ * React context that propagates a parent dismissible layer's node down the
95
+ * React tree (including through portals). Nested dismissible layers read this
96
+ * to register as children of the parent, enabling correct cascade-dismiss
97
+ * behavior without affecting unrelated sibling layers.
98
+ *
99
+ * Follows the same pattern as base-ui's FloatingNodeContext.
100
+ */ const DismissibleParentContext = createContext(null);
101
+ function useDismissibleParentNode() {
102
+ return useContext(DismissibleParentContext);
103
+ }
104
+ function addBranch(ctx, node) {
105
+ ctx.branches.push(node);
106
+ }
107
+ function removeBranch(ctx, node) {
108
+ const index = ctx.branches.indexOf(node);
109
+ if (index >= 0) ctx.branches.splice(index, 1);
110
+ }
111
+
112
+ export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b, isBelowPointerBlockingLayer as c, useDismissibleParentNode as d, addLayer as e, LayerStackContext as f, getPointerEventsEnabled as g, addBranch as h, isTopMost as i, removeBranch as j, removeLayer as r, useLayerStackContext as u };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { useRef, useEffect } from 'react';
3
+ import { i as isTopMost } from './layer-stack-12s-DjRVp1_u.js';
4
+
5
+ /**
6
+ * Tracks escape keydown on the document (capture phase).
7
+ * Only fires callback when the given node is the topmost layer.
8
+ */ function useEscapeKeydown(node, ctx, onEscapeKeyDown) {
9
+ const callbackRef = useRef(onEscapeKeyDown);
10
+ callbackRef.current = onEscapeKeyDown;
11
+ useEffect(()=>{
12
+ if (!node) return;
13
+ const handler = (event)=>{
14
+ if (event.key !== "Escape") return;
15
+ if (event.isComposing) return;
16
+ if (!isTopMost(ctx, node)) return;
17
+ callbackRef.current(event);
18
+ };
19
+ document.addEventListener("keydown", handler, {
20
+ capture: true
21
+ });
22
+ return ()=>document.removeEventListener("keydown", handler, {
23
+ capture: true
24
+ });
25
+ }, [
26
+ node,
27
+ ctx
28
+ ]);
29
+ }
30
+
31
+ export { useEscapeKeydown as u };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ var react = require('react');
3
+ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
4
+
5
+ /**
6
+ * Tracks escape keydown on the document (capture phase).
7
+ * Only fires callback when the given node is the topmost layer.
8
+ */ function useEscapeKeydown(node, ctx, onEscapeKeyDown) {
9
+ const callbackRef = react.useRef(onEscapeKeyDown);
10
+ callbackRef.current = onEscapeKeyDown;
11
+ react.useEffect(()=>{
12
+ if (!node) return;
13
+ const handler = (event)=>{
14
+ if (event.key !== "Escape") return;
15
+ if (event.isComposing) return;
16
+ if (!layerStack12s.isTopMost(ctx, node)) return;
17
+ callbackRef.current(event);
18
+ };
19
+ document.addEventListener("keydown", handler, {
20
+ capture: true
21
+ });
22
+ return ()=>document.removeEventListener("keydown", handler, {
23
+ capture: true
24
+ });
25
+ }, [
26
+ node,
27
+ ctx
28
+ ]);
29
+ }
30
+
31
+ exports.useEscapeKeydown = useEscapeKeydown;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ var react = require('react');
3
+ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
4
+
5
+ /**
6
+ * Detects focus events outside a React subtree.
7
+ * Uses `onFocusCapture`/`onBlurCapture` for React-tree-based detection.
8
+ *
9
+ * Ported from Radix's useFocusOutside with layer stack integration.
10
+ */ function useFocusOutside(node, ctx, options) {
11
+ const callbackRef = react.useRef(options.onFocusOutside);
12
+ callbackRef.current = options.onFocusOutside;
13
+ const excludeRef = react.useRef(options.exclude);
14
+ excludeRef.current = options.exclude;
15
+ const isFocusInsideReactTreeRef = react.useRef(false);
16
+ react.useEffect(()=>{
17
+ if (!node || !options.enabled) return;
18
+ const ownerDocument = node.ownerDocument ?? document;
19
+ const handleFocus = (event)=>{
20
+ const target = event.target;
21
+ if (target && !isFocusInsideReactTreeRef.current) {
22
+ if (layerStack12s.isInBranch(ctx, target)) return;
23
+ if (layerStack12s.isInNestedLayer(ctx, node, target)) return;
24
+ if (target instanceof HTMLElement && excludeRef.current?.(target)) return;
25
+ callbackRef.current(event);
26
+ }
27
+ };
28
+ ownerDocument.addEventListener("focusin", handleFocus);
29
+ return ()=>{
30
+ ownerDocument.removeEventListener("focusin", handleFocus);
31
+ isFocusInsideReactTreeRef.current = false;
32
+ };
33
+ }, [
34
+ node,
35
+ ctx,
36
+ options.enabled
37
+ ]);
38
+ return {
39
+ onFocusCapture: ()=>{
40
+ isFocusInsideReactTreeRef.current = true;
41
+ },
42
+ onBlurCapture: ()=>{
43
+ isFocusInsideReactTreeRef.current = false;
44
+ }
45
+ };
46
+ }
47
+
48
+ exports.useFocusOutside = useFocusOutside;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { useRef, useEffect } from 'react';
3
+ import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-DjRVp1_u.js';
4
+
5
+ /**
6
+ * Detects focus events outside a React subtree.
7
+ * Uses `onFocusCapture`/`onBlurCapture` for React-tree-based detection.
8
+ *
9
+ * Ported from Radix's useFocusOutside with layer stack integration.
10
+ */ function useFocusOutside(node, ctx, options) {
11
+ const callbackRef = useRef(options.onFocusOutside);
12
+ callbackRef.current = options.onFocusOutside;
13
+ const excludeRef = useRef(options.exclude);
14
+ excludeRef.current = options.exclude;
15
+ const isFocusInsideReactTreeRef = useRef(false);
16
+ useEffect(()=>{
17
+ if (!node || !options.enabled) return;
18
+ const ownerDocument = node.ownerDocument ?? document;
19
+ const handleFocus = (event)=>{
20
+ const target = event.target;
21
+ if (target && !isFocusInsideReactTreeRef.current) {
22
+ if (isInBranch(ctx, target)) return;
23
+ if (isInNestedLayer(ctx, node, target)) return;
24
+ if (target instanceof HTMLElement && excludeRef.current?.(target)) return;
25
+ callbackRef.current(event);
26
+ }
27
+ };
28
+ ownerDocument.addEventListener("focusin", handleFocus);
29
+ return ()=>{
30
+ ownerDocument.removeEventListener("focusin", handleFocus);
31
+ isFocusInsideReactTreeRef.current = false;
32
+ };
33
+ }, [
34
+ node,
35
+ ctx,
36
+ options.enabled
37
+ ]);
38
+ return {
39
+ onFocusCapture: ()=>{
40
+ isFocusInsideReactTreeRef.current = true;
41
+ },
42
+ onBlurCapture: ()=>{
43
+ isFocusInsideReactTreeRef.current = false;
44
+ }
45
+ };
46
+ }
47
+
48
+ export { useFocusOutside as u };