@askrjs/askr 0.0.8 → 0.0.9

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.
@@ -94,6 +94,133 @@ interface Portal<T = unknown> {
94
94
  declare function definePortal<T = unknown>(): Portal<T>;
95
95
  declare const DefaultPortal: Portal<unknown>;
96
96
 
97
+ /**
98
+ * createCollection
99
+ *
100
+ * Ordered descendant registry for coordinating items without DOM queries.
101
+ *
102
+ * INVARIANTS:
103
+ * 1. Registration order determines item order (no DOM queries)
104
+ * 2. Stable ordering across renders (insertion order preserved)
105
+ * 3. Each item may have metadata (type-safe, user-defined)
106
+ * 4. No implicit global state (explicit collection instances)
107
+ * 5. No automatic cleanup (caller controls lifecycle)
108
+ *
109
+ * DESIGN:
110
+ * - Returns a registry API ({ register, items, clear })
111
+ * - Items are stored in insertion order
112
+ * - Registration returns an unregister function
113
+ * - No side effects on registration (pure data structure)
114
+ *
115
+ * USAGE:
116
+ * const collection = createCollection<HTMLElement, { disabled: boolean }>();
117
+ * const unregister = collection.register(element, { disabled: false });
118
+ * const allItems = collection.items();
119
+ * unregister();
120
+ */
121
+ type CollectionItem<TNode, TMetadata = unknown> = {
122
+ node: TNode;
123
+ metadata: TMetadata;
124
+ };
125
+ interface Collection<TNode, TMetadata = unknown> {
126
+ /**
127
+ * Register a node with optional metadata.
128
+ * Returns an unregister function.
129
+ */
130
+ register(node: TNode, metadata: TMetadata): () => void;
131
+ /**
132
+ * Get all registered items in insertion order.
133
+ */
134
+ items(): ReadonlyArray<CollectionItem<TNode, TMetadata>>;
135
+ /**
136
+ * Clear all registered items.
137
+ */
138
+ clear(): void;
139
+ /**
140
+ * Get the count of registered items.
141
+ */
142
+ size(): number;
143
+ }
144
+ declare function createCollection<TNode, TMetadata = unknown>(): Collection<TNode, TMetadata>;
145
+
146
+ /**
147
+ * createLayer
148
+ *
149
+ * Manages stacking order and coordination for overlays (modals, popovers, etc).
150
+ *
151
+ * INVARIANTS:
152
+ * 1. Layers are ordered by registration time (FIFO)
153
+ * 2. Only the top layer handles Escape key
154
+ * 3. Only the top layer handles outside pointer events
155
+ * 4. Nested layers are supported
156
+ * 5. Does not implement portals (orthogonal concern)
157
+ * 6. No automatic DOM insertion (caller controls mounting)
158
+ *
159
+ * DESIGN:
160
+ * - Returns a layer manager with register/unregister API
161
+ * - Each layer has a unique ID and can query if it's the top layer
162
+ * - Escape and outside pointer coordination via callbacks
163
+ * - No z-index management (CSS concern)
164
+ *
165
+ * USAGE:
166
+ * const manager = createLayer();
167
+ *
168
+ * const layer = manager.register({
169
+ * onEscape: () => { ... },
170
+ * onOutsidePointer: () => { ... }
171
+ * });
172
+ *
173
+ * layer.isTop(); // true if this is the topmost layer
174
+ * layer.unregister();
175
+ */
176
+ interface LayerOptions {
177
+ /**
178
+ * Called when Escape is pressed and this is the top layer
179
+ */
180
+ onEscape?: () => void;
181
+ /**
182
+ * Called when pointer event occurs outside and this is the top layer
183
+ */
184
+ onOutsidePointer?: (e: PointerEvent) => void;
185
+ /**
186
+ * Optional node reference for outside pointer detection
187
+ */
188
+ node?: Node | null;
189
+ }
190
+ interface Layer {
191
+ /**
192
+ * Unique layer ID
193
+ */
194
+ id: number;
195
+ /**
196
+ * Check if this layer is the topmost
197
+ */
198
+ isTop(): boolean;
199
+ /**
200
+ * Remove this layer from the stack
201
+ */
202
+ unregister(): void;
203
+ }
204
+ interface LayerManager {
205
+ /**
206
+ * Register a new layer
207
+ */
208
+ register(options: LayerOptions): Layer;
209
+ /**
210
+ * Get all active layers in order
211
+ */
212
+ layers(): ReadonlyArray<Layer>;
213
+ /**
214
+ * Manually trigger escape handling on the top layer
215
+ */
216
+ handleEscape(): void;
217
+ /**
218
+ * Manually trigger outside pointer handling on the top layer
219
+ */
220
+ handleOutsidePointer(e: PointerEvent): void;
221
+ }
222
+ declare function createLayer(): LayerManager;
223
+
97
224
  /**
98
225
  * composeHandlers
99
226
  *
@@ -319,22 +446,59 @@ declare function pressable({ disabled, onPress, isNativeButton, }: PressableOpti
319
446
  /**
320
447
  * dismissable
321
448
  *
322
- * Provides props and helpers to support dismissal behaviour. This helper is
323
- * runtime-agnostic:
324
- * - It returns `onKeyDown` prop which will call onDismiss when Escape is
325
- * pressed.
326
- * - It also provides `outsideListener` factory which given an `isInside`
327
- * predicate returns a handler suitable to attach at the document level that
328
- * will call onDismiss when the pointerdown target is outside the component.
449
+ * THE dismissal primitive. Handles Escape key and outside interactions.
450
+ *
451
+ * INVARIANTS:
452
+ * 1. Returns props that compose via mergeProps (no factories)
453
+ * 2. Disabled state respected exactly once, here
454
+ * 3. No side effects - pure props generation
455
+ * 4. Outside detection requires explicit node reference
456
+ * 5. This is the ONLY dismissal primitive - do not create alternatives
457
+ *
458
+ * DESIGN:
459
+ * - Returns standard event handler props (onKeyDown, onPointerDownCapture)
460
+ * - Composable via mergeProps with other foundations
461
+ * - Caller provides node reference for outside detection
462
+ * - Single onDismiss callback for all dismiss triggers
463
+ *
464
+ * PIT OF SUCCESS:
465
+ * ✓ Can't accidentally bypass (only way to get dismiss behavior)
466
+ * ✓ Can't duplicate (disabled checked once)
467
+ * ✓ Composes via mergeProps (standard props)
468
+ * ✓ Wrong usage is hard (no factories to misuse)
469
+ *
470
+ * USAGE:
471
+ * const props = dismissable({
472
+ * node: elementRef,
473
+ * disabled: false,
474
+ * onDismiss: () => close()
475
+ * });
476
+ *
477
+ * <div ref={elementRef} {...props}>Content</div>
478
+ *
479
+ * MISUSE EXAMPLE (PREVENTED):
480
+ * ❌ Can't forget to check disabled - checked inside dismissable
481
+ * ❌ Can't create custom escape handler - this is the only one
482
+ * ❌ Can't bypass via direct event listeners - mergeProps composes correctly
329
483
  */
330
484
  interface DismissableOptions {
331
- onDismiss?: () => void;
485
+ /**
486
+ * Reference to the element for outside click detection
487
+ */
488
+ node?: Node | null;
489
+ /**
490
+ * Whether dismiss is disabled
491
+ */
332
492
  disabled?: boolean;
493
+ /**
494
+ * Called when dismiss is triggered (Escape or outside click)
495
+ */
496
+ onDismiss?: (trigger: 'escape' | 'outside') => void;
333
497
  }
334
498
 
335
- declare function dismissable({ onDismiss, disabled }: DismissableOptions): {
336
- onKeyDown: ((e: KeyboardLikeEvent) => void) | undefined;
337
- outsideListener: ((isInside: (target: unknown) => boolean) => (e: PointerLikeEvent) => void) | undefined;
499
+ declare function dismissable({ node, disabled, onDismiss, }: DismissableOptions): {
500
+ onKeyDown: (e: KeyboardLikeEvent) => void;
501
+ onPointerDownCapture: (e: PointerLikeEvent) => void;
338
502
  };
339
503
 
340
504
  /**
@@ -371,4 +535,190 @@ interface HoverableResult {
371
535
  }
372
536
  declare function hoverable({ disabled, onEnter, onLeave, }: HoverableOptions): HoverableResult;
373
537
 
374
- export { type ComposeHandlersOptions, type ControllableState, DefaultPortal, type DismissableOptions, type FocusableOptions, type FocusableResult, type HoverableOptions, type HoverableResult, JSXElement, type Portal, Presence, type PresenceProps, type PressableOptions, type PressableResult, type Ref, Slot, type SlotProps, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, definePortal, dismissable, focusable, hoverable, isControlled, makeControllable, mergeProps, pressable, resolveControllable, setRef, useId };
538
+ /**
539
+ * rovingFocus
540
+ *
541
+ * Single tab stop navigation with arrow-key control.
542
+ *
543
+ * INVARIANTS:
544
+ * 1. Only one item in the group is reachable via Tab (single tab stop)
545
+ * 2. Arrow keys move focus within the group
546
+ * 3. Orientation determines which arrow keys are active
547
+ * 4. Looping is opt-in
548
+ * 5. Disabled items are skipped
549
+ * 6. Returns props objects, never factories (composes via mergeProps)
550
+ *
551
+ * DESIGN:
552
+ * - Container gets onKeyDown for arrow navigation
553
+ * - Each item gets tabIndex based on current selection
554
+ * - Navigation logic is pure - caller controls focus application
555
+ * - Disabled check happens per-item via predicate
556
+ *
557
+ * PIT OF SUCCESS:
558
+ * ✓ Can't accidentally break tab order (tabIndex assigned correctly)
559
+ * ✓ Can't duplicate navigation logic (single source)
560
+ * ✓ Composes via mergeProps (all standard props)
561
+ * ✓ Type-safe - invalid indices caught at call site
562
+ *
563
+ * USAGE:
564
+ * const nav = rovingFocus({
565
+ * currentIndex: 0,
566
+ * itemCount: 3,
567
+ * orientation: 'horizontal',
568
+ * onNavigate: setIndex
569
+ * });
570
+ *
571
+ * <div {...nav.container}>
572
+ * <button {...nav.item(0)}>First</button>
573
+ * <button {...nav.item(1)}>Second</button>
574
+ * </div>
575
+ *
576
+ * MISUSE EXAMPLE (PREVENTED):
577
+ * ❌ Can't forget to set tabIndex - returned in item props
578
+ * ❌ Can't create conflicting arrow handlers - mergeProps composes
579
+ * ❌ Can't skip disabled items incorrectly - logic is internal
580
+ */
581
+
582
+ type Orientation = 'horizontal' | 'vertical' | 'both';
583
+ interface RovingFocusOptions {
584
+ /**
585
+ * Current focused index
586
+ */
587
+ currentIndex: number;
588
+ /**
589
+ * Total number of items
590
+ */
591
+ itemCount: number;
592
+ /**
593
+ * Navigation orientation
594
+ * - horizontal: ArrowLeft/ArrowRight
595
+ * - vertical: ArrowUp/ArrowDown
596
+ * - both: all arrow keys
597
+ */
598
+ orientation?: Orientation;
599
+ /**
600
+ * Whether to loop when reaching the end
601
+ */
602
+ loop?: boolean;
603
+ /**
604
+ * Callback when navigation occurs
605
+ */
606
+ onNavigate?: (index: number) => void;
607
+ /**
608
+ * Optional disabled state check per index
609
+ */
610
+ isDisabled?: (index: number) => boolean;
611
+ }
612
+ interface RovingFocusResult {
613
+ /**
614
+ * Props for the container element (composes via mergeProps)
615
+ */
616
+ container: {
617
+ onKeyDown: (e: KeyboardLikeEvent) => void;
618
+ };
619
+ /**
620
+ * Generate props for an item at the given index (composes via mergeProps)
621
+ */
622
+ item: (index: number) => {
623
+ tabIndex: number;
624
+ 'data-roving-index': number;
625
+ };
626
+ }
627
+ declare function rovingFocus(options: RovingFocusOptions): RovingFocusResult;
628
+
629
+ /**
630
+ * INTERACTION POLICY (FRAMEWORK LAW)
631
+ *
632
+ * This is THE ONLY way to create interactive elements. Components MUST NOT
633
+ * implement interaction logic directly.
634
+ *
635
+ * INVARIANTS (ENFORCED):
636
+ * 1. "Press" is the semantic. Click/touch/Enter/Space are implementation details.
637
+ * 2. Disabled is enforced exactly once, here. Components may not check disabled.
638
+ * 3. Keyboard handling is automatic. Components may not add onKeyDown for activation.
639
+ * 4. Native elements opt out of polyfills, not semantics.
640
+ * 5. asChild may replace the host element, not interaction behavior.
641
+ * 6. This policy is the SINGLE SOURCE OF TRUTH for interactive behavior.
642
+ *
643
+ * DESIGN:
644
+ * - Single public function: applyInteractionPolicy
645
+ * - Returns props that compose via mergeProps
646
+ * - Delegates to pressable for mechanics
647
+ * - Enforces disabled once and only once
648
+ * - No configuration beyond disabled and native element type
649
+ *
650
+ * PIT OF SUCCESS:
651
+ * ✓ Can't bypass policy (only way to get interaction behavior)
652
+ * ✓ Can't duplicate disabled checks (enforced once, here)
653
+ * ✓ Can't write custom keyboard handlers for buttons (policy owns it)
654
+ * ✓ Composes via mergeProps (standard props)
655
+ * ✓ Wrong usage is impossible (no escape hatch)
656
+ *
657
+ * USAGE:
658
+ * function Button({ onPress, disabled }) {
659
+ * const interaction = applyInteractionPolicy({
660
+ * isNative: true,
661
+ * disabled,
662
+ * onPress
663
+ * });
664
+ *
665
+ * return <button {...interaction}>Click me</button>;
666
+ * }
667
+ *
668
+ * MISUSE EXAMPLE (PREVENTED):
669
+ * ❌ Button checking disabled again:
670
+ * function Button({ disabled, onPress }) {
671
+ * if (disabled) return; // NO! Policy handles this
672
+ * const interaction = applyInteractionPolicy(...);
673
+ * }
674
+ *
675
+ * ❌ Custom keyboard handler:
676
+ * function Button({ onPress }) {
677
+ * const interaction = applyInteractionPolicy(...);
678
+ * return <button {...interaction} onKeyDown={...}>; // NO! Policy owns this
679
+ * }
680
+ *
681
+ * ❌ Direct event handler:
682
+ * <button onClick={onPress}>; // NO! Use applyInteractionPolicy
683
+ */
684
+ interface InteractionPolicyInput {
685
+ /** Whether the host element is a native interactive element (button, a, etc) */
686
+ isNative: boolean;
687
+ /** Disabled state - checked ONLY here, never in components */
688
+ disabled: boolean;
689
+ /** User-provided press handler - semantic action, not DOM event */
690
+ onPress?: (e: Event) => void;
691
+ /** Optional ref to compose */
692
+ ref?: any;
693
+ }
694
+ /**
695
+ * THE interaction policy. Components MUST use this, NEVER implement
696
+ * interaction logic directly.
697
+ */
698
+ declare function applyInteractionPolicy({ isNative, disabled, onPress, ref, }: InteractionPolicyInput): {
699
+ disabled: true | undefined;
700
+ onClick: (e: Event) => void;
701
+ ref: any;
702
+ } | {
703
+ 'aria-disabled': true | undefined;
704
+ tabIndex: number;
705
+ ref: any;
706
+ onClick: (e: DefaultPreventable & PropagationStoppable) => void;
707
+ disabled?: true;
708
+ role?: "button";
709
+ onKeyDown?: (e: KeyboardLikeEvent) => void;
710
+ onKeyUp?: (e: KeyboardLikeEvent) => void;
711
+ };
712
+ /**
713
+ * Merge rule for Slot / asChild
714
+ *
715
+ * Precedence:
716
+ * policy → user → child
717
+ *
718
+ * Event handlers are composed (policy first).
719
+ * Refs are always composed.
720
+ * Policy props MUST take precedence to enforce invariants.
721
+ */
722
+ declare function mergeInteractionProps(childProps: Record<string, any>, policyProps: Record<string, any>, userProps?: Record<string, any>): Record<string, any>;
723
+
724
+ export { type Collection, type CollectionItem, type ComposeHandlersOptions, type ControllableState, DefaultPortal, type DismissableOptions, type FocusableOptions, type FocusableResult, type HoverableOptions, type HoverableResult, type InteractionPolicyInput, JSXElement, type Layer, type LayerManager, type LayerOptions, type Orientation, type Portal, Presence, type PresenceProps, type PressableOptions, type PressableResult, type Ref, type RovingFocusOptions, type RovingFocusResult, Slot, type SlotProps, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, hoverable, isControlled, makeControllable, mergeInteractionProps, mergeProps, pressable, resolveControllable, rovingFocus, setRef, useId };
@@ -98,7 +98,95 @@ var DefaultPortal = (() => {
98
98
  return Host;
99
99
  })();
100
100
 
101
- // src/foundations/utilities/composeHandlers.ts
101
+ // src/foundations/structures/collection.ts
102
+ function createCollection() {
103
+ const registry = /* @__PURE__ */ new Map();
104
+ function register(node, metadata) {
105
+ const item = { node, metadata };
106
+ registry.set(node, item);
107
+ return () => {
108
+ registry.delete(node);
109
+ };
110
+ }
111
+ function items() {
112
+ return Array.from(registry.values());
113
+ }
114
+ function clear() {
115
+ registry.clear();
116
+ }
117
+ function size() {
118
+ return registry.size;
119
+ }
120
+ return {
121
+ register,
122
+ items,
123
+ clear,
124
+ size
125
+ };
126
+ }
127
+
128
+ // src/foundations/structures/layer.ts
129
+ function createLayer() {
130
+ const stack = [];
131
+ let nextId = 1;
132
+ function register(options) {
133
+ const id = nextId++;
134
+ const entry = { id, options };
135
+ stack.push(entry);
136
+ function isTop() {
137
+ return stack[stack.length - 1]?.id === id;
138
+ }
139
+ function unregister() {
140
+ const index = stack.findIndex((e) => e.id === id);
141
+ if (index !== -1) {
142
+ stack.splice(index, 1);
143
+ }
144
+ }
145
+ return {
146
+ id,
147
+ isTop,
148
+ unregister
149
+ };
150
+ }
151
+ function layers() {
152
+ return stack.map((entry) => ({
153
+ id: entry.id,
154
+ isTop: () => stack[stack.length - 1]?.id === entry.id,
155
+ unregister: () => {
156
+ const index = stack.findIndex((e) => e.id === entry.id);
157
+ if (index !== -1) {
158
+ stack.splice(index, 1);
159
+ }
160
+ }
161
+ }));
162
+ }
163
+ function handleEscape() {
164
+ const top = stack[stack.length - 1];
165
+ if (top) {
166
+ top.options.onEscape?.();
167
+ }
168
+ }
169
+ function handleOutsidePointer(e) {
170
+ const top = stack[stack.length - 1];
171
+ if (!top) return;
172
+ const node = top.options.node;
173
+ if (node && e.target instanceof Node) {
174
+ if (!node.contains(e.target)) {
175
+ top.options.onOutsidePointer?.(e);
176
+ }
177
+ } else {
178
+ top.options.onOutsidePointer?.(e);
179
+ }
180
+ }
181
+ return {
182
+ register,
183
+ layers,
184
+ handleEscape,
185
+ handleOutsidePointer
186
+ };
187
+ }
188
+
189
+ // src/foundations/utilities/compose-handlers.ts
102
190
  function isDefaultPrevented(value) {
103
191
  return typeof value === "object" && value !== null && "defaultPrevented" in value && value.defaultPrevented === true;
104
192
  }
@@ -120,7 +208,7 @@ function composeHandlers(first, second, options) {
120
208
  function noop() {
121
209
  }
122
210
 
123
- // src/foundations/utilities/mergeProps.ts
211
+ // src/foundations/utilities/merge-props.ts
124
212
  function isEventHandlerKey(key) {
125
213
  return key.startsWith("on");
126
214
  }
@@ -156,7 +244,7 @@ function ariaSelected(selected) {
156
244
  return selected === void 0 ? {} : { "aria-selected": String(selected) };
157
245
  }
158
246
 
159
- // src/foundations/utilities/composeRef.ts
247
+ // src/foundations/utilities/compose-ref.ts
160
248
  function setRef(ref, value) {
161
249
  if (!ref) return;
162
250
  if (typeof ref === "function") {
@@ -174,7 +262,7 @@ function composeRefs(...refs) {
174
262
  };
175
263
  }
176
264
 
177
- // src/foundations/utilities/useId.ts
265
+ // src/foundations/utilities/use-id.ts
178
266
  function useId(options) {
179
267
  const prefix = options.prefix ?? "askr";
180
268
  return `${prefix}-${String(options.id)}`;
@@ -1020,24 +1108,32 @@ function pressable({
1020
1108
  }
1021
1109
 
1022
1110
  // src/foundations/interactions/dismissable.ts
1023
- function dismissable({ onDismiss, disabled }) {
1024
- return {
1025
- // Prop for the component root to handle Escape
1026
- onKeyDown: disabled ? void 0 : (e) => {
1027
- if (e.key === "Escape") {
1028
- e.preventDefault?.();
1029
- e.stopPropagation?.();
1030
- onDismiss?.();
1031
- }
1032
- },
1033
- // Factory: runtime should attach this listener at the appropriate scope.
1034
- outsideListener: disabled ? void 0 : (isInside) => (e) => {
1035
- if (!isInside(e.target)) {
1036
- e.preventDefault?.();
1037
- e.stopPropagation?.();
1038
- onDismiss?.();
1039
- }
1111
+ function dismissable({
1112
+ node,
1113
+ disabled,
1114
+ onDismiss
1115
+ }) {
1116
+ function handleKeyDown(e) {
1117
+ if (disabled) return;
1118
+ if (e.key === "Escape") {
1119
+ e.preventDefault?.();
1120
+ e.stopPropagation?.();
1121
+ onDismiss?.("escape");
1040
1122
  }
1123
+ }
1124
+ function handlePointerDownCapture(e) {
1125
+ if (disabled) return;
1126
+ const target = e.target;
1127
+ if (!(target instanceof Node)) return;
1128
+ if (!node) return;
1129
+ if (!node.contains(target)) {
1130
+ onDismiss?.("outside");
1131
+ }
1132
+ }
1133
+ return {
1134
+ onKeyDown: handleKeyDown,
1135
+ // Use capture phase to catch events before they bubble
1136
+ onPointerDownCapture: handlePointerDownCapture
1041
1137
  };
1042
1138
  }
1043
1139
 
@@ -1068,6 +1164,111 @@ function hoverable({
1068
1164
  };
1069
1165
  }
1070
1166
 
1071
- export { DefaultPortal, Presence, Slot, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, definePortal, dismissable, focusable, hoverable, isControlled, layout, makeControllable, mergeProps, pressable, resolveControllable, setRef, useId };
1167
+ // src/foundations/interactions/roving-focus.ts
1168
+ function rovingFocus(options) {
1169
+ const {
1170
+ currentIndex,
1171
+ itemCount,
1172
+ orientation = "horizontal",
1173
+ loop = false,
1174
+ onNavigate,
1175
+ isDisabled
1176
+ } = options;
1177
+ function findNextIndex(from, direction) {
1178
+ let next = from + direction;
1179
+ if (loop) {
1180
+ if (next < 0) next = itemCount - 1;
1181
+ if (next >= itemCount) next = 0;
1182
+ } else {
1183
+ if (next < 0 || next >= itemCount) return void 0;
1184
+ }
1185
+ if (isDisabled?.(next)) {
1186
+ if (next === from) return void 0;
1187
+ return findNextIndex(next, direction);
1188
+ }
1189
+ return next;
1190
+ }
1191
+ function handleKeyDown(e) {
1192
+ const { key } = e;
1193
+ let direction;
1194
+ if (orientation === "horizontal" || orientation === "both") {
1195
+ if (key === "ArrowRight") direction = 1;
1196
+ if (key === "ArrowLeft") direction = -1;
1197
+ }
1198
+ if (orientation === "vertical" || orientation === "both") {
1199
+ if (key === "ArrowDown") direction = 1;
1200
+ if (key === "ArrowUp") direction = -1;
1201
+ }
1202
+ if (direction === void 0) return;
1203
+ const nextIndex = findNextIndex(currentIndex, direction);
1204
+ if (nextIndex === void 0) return;
1205
+ e.preventDefault?.();
1206
+ e.stopPropagation?.();
1207
+ onNavigate?.(nextIndex);
1208
+ }
1209
+ return {
1210
+ container: {
1211
+ onKeyDown: handleKeyDown
1212
+ },
1213
+ item: (index) => ({
1214
+ tabIndex: index === currentIndex ? 0 : -1,
1215
+ "data-roving-index": index
1216
+ })
1217
+ };
1218
+ }
1219
+
1220
+ // src/foundations/interactions/interaction-policy.ts
1221
+ function applyInteractionPolicy({
1222
+ isNative,
1223
+ disabled,
1224
+ onPress,
1225
+ ref
1226
+ }) {
1227
+ function invokePress(e) {
1228
+ if (disabled) {
1229
+ e.preventDefault?.();
1230
+ return;
1231
+ }
1232
+ onPress?.(e);
1233
+ }
1234
+ if (isNative) {
1235
+ return {
1236
+ disabled: disabled || void 0,
1237
+ onClick: (e) => invokePress(e),
1238
+ ref
1239
+ };
1240
+ }
1241
+ const interaction = pressable({
1242
+ disabled,
1243
+ isNativeButton: false,
1244
+ onPress: (e) => invokePress(e)
1245
+ });
1246
+ return {
1247
+ ...interaction,
1248
+ "aria-disabled": disabled || void 0,
1249
+ tabIndex: disabled ? -1 : interaction.tabIndex ?? 0,
1250
+ ref
1251
+ };
1252
+ }
1253
+ function mergeInteractionProps(childProps, policyProps, userProps) {
1254
+ let out = mergeProps(childProps, policyProps);
1255
+ if (userProps) out = mergeProps(out, userProps);
1256
+ for (const k in out) {
1257
+ if (!k.startsWith("on")) continue;
1258
+ const policyHandler = policyProps?.[k];
1259
+ const userHandler = userProps?.[k];
1260
+ const childHandler = childProps?.[k];
1261
+ if (policyHandler || userHandler || childHandler) {
1262
+ out[k] = composeHandlers(
1263
+ policyHandler,
1264
+ composeHandlers(userHandler, childHandler)
1265
+ );
1266
+ }
1267
+ }
1268
+ out.ref = composeRefs(childProps?.ref, userProps?.ref, policyProps?.ref);
1269
+ return out;
1270
+ }
1271
+
1272
+ export { DefaultPortal, Presence, Slot, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, hoverable, isControlled, layout, makeControllable, mergeInteractionProps, mergeProps, pressable, resolveControllable, rovingFocus, setRef, useId };
1072
1273
  //# sourceMappingURL=index.js.map
1073
1274
  //# sourceMappingURL=index.js.map