forty-cdk 0.2.0 → 0.3.0

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 (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,1145 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, ElementRef, model, input, booleanAttribute, signal, linkedSignal, DestroyRef, computed, Directive, afterNextRender, effect, PLATFORM_ID } from '@angular/core';
3
+ import { createDefaults, injectTextDirection, Collection, createDebouncedAction, injectDismissableLayer, nextEnabledHandle, hostId, registerHandle, reflectDisabled, resolveListNavigation } from 'forty-cdk/core';
4
+ import { isPlatformBrowser } from '@angular/common';
5
+
6
+ const FOR_NAVIGATION_MENU_CONTEXT = new InjectionToken('FOR_NAVIGATION_MENU_CONTEXT');
7
+ const FOR_NAVIGATION_MENU_ITEM_CONTEXT = new InjectionToken('FOR_NAVIGATION_MENU_ITEM_CONTEXT');
8
+ function injectNavigationMenuContext(piece) {
9
+ const ctx = inject(FOR_NAVIGATION_MENU_CONTEXT, { optional: true });
10
+ if (!ctx) {
11
+ throw new Error(`[forty-cdk/navigation-menu] ${piece} must be used inside a [forNavigationMenu] element. ` +
12
+ "If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
13
+ 'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
14
+ '[forNavigationMenu] root.');
15
+ }
16
+ return ctx;
17
+ }
18
+ function injectNavigationMenuItemContext(piece) {
19
+ const ctx = inject(FOR_NAVIGATION_MENU_ITEM_CONTEXT, { optional: true });
20
+ if (!ctx) {
21
+ throw new Error(`[forty-cdk/navigation-menu] ${piece} must be used inside a [forNavigationMenuItem] element. ` +
22
+ "If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
23
+ 'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
24
+ '[forNavigationMenuItem] root.');
25
+ }
26
+ return ctx;
27
+ }
28
+
29
+ /**
30
+ * Library fallback for navigation menu defaults, read at the root injector when no
31
+ * consumer has called `provideForNavigationMenuDefaults`. Exported for the shared defaults
32
+ * contract spec; not re-exported from the primitive's public entry.
33
+ */
34
+ const FOR_NAVIGATION_MENU_FALLBACK_DEFAULTS = {
35
+ delayDuration: 200,
36
+ closeDelay: 150,
37
+ skipDelayDuration: 300,
38
+ };
39
+ const { token, provideDefaults } = createDefaults('FOR_NAVIGATION_MENU_DEFAULTS', FOR_NAVIGATION_MENU_FALLBACK_DEFAULTS);
40
+ /** Token holding the resolved navigation-menu defaults for the current scope. */
41
+ const FOR_NAVIGATION_MENU_DEFAULTS = token;
42
+ /**
43
+ * Configures forty-cdk navigation-menu defaults for this injector scope.
44
+ * Partial overrides inherit unspecified keys from the parent scope (or
45
+ * library defaults at the root).
46
+ */
47
+ function provideForNavigationMenuDefaults(defaults = {}) {
48
+ return provideDefaults(defaults);
49
+ }
50
+
51
+ /**
52
+ * Headless implementation of the
53
+ * [WAI-ARIA Disclosure Navigation Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/).
54
+ *
55
+ * Despite the name, this is a `<nav>` of disclosures — NOT an ARIA `menu`.
56
+ * Triggers are buttons with `aria-expanded`, content is regular landmarks
57
+ * with links inside; Tab moves through links; Escape closes the open item
58
+ * and returns focus to its trigger.
59
+ *
60
+ * One item is open at a time. Selection is exposed as `value` (the open
61
+ * item's id, or `''` when nothing is open). The `model()` change emitter
62
+ * fires only on internal transitions, never on consumer writes.
63
+ *
64
+ * Submenús anidados están fuera de scope para v1.
65
+ *
66
+ * @example
67
+ * ```html
68
+ * <nav forNavigationMenu aria-label="Main">
69
+ * <ul forNavigationMenuList>
70
+ * <li forNavigationMenuItem value="products">
71
+ * <button forNavigationMenuTrigger>Products</button>
72
+ * @if (products.isOpen()) {
73
+ * <div forNavigationMenuContent>…</div>
74
+ * }
75
+ * </li>
76
+ * </ul>
77
+ * </nav>
78
+ * ```
79
+ */
80
+ class ForNavigationMenu {
81
+ #host = inject(ElementRef);
82
+ #defaults = inject(FOR_NAVIGATION_MENU_DEFAULTS);
83
+ /**
84
+ * Two-way bindable. The id of the open item, or `''` for none. The
85
+ * `model()` change emitter (`(valueChange)`) fires only on internal
86
+ * transitions (trigger click, hover delay, Escape, outside click), never
87
+ * on consumer writes via `[(value)]`.
88
+ */
89
+ value = model('', /* @ts-ignore */
90
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
91
+ orientation = input('horizontal', /* @ts-ignore */
92
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
93
+ /**
94
+ * Writing direction. When unset (default `null`), the inherited ambient
95
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
96
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
97
+ * The resolved value is reflected to the host `dir` attribute and swaps
98
+ * ArrowLeft / ArrowRight semantics in RTL.
99
+ */
100
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
101
+ dir = injectTextDirection(this._dirInput);
102
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
103
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
104
+ /**
105
+ * Manual `aria-label` for the surrounding `<nav>`. Use this when no visible
106
+ * label element exists; otherwise prefer pointing `aria-labelledby` at one.
107
+ * A `null` (default) or empty value emits no attribute.
108
+ */
109
+ ariaLabel = input(null, /* @ts-ignore */
110
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
111
+ /**
112
+ * ms before a hover opens an item. Default `200`. The default is read from
113
+ * `provideForNavigationMenuDefaults` for the surrounding scope.
114
+ */
115
+ delayDuration = input(this.#defaults.delayDuration, /* @ts-ignore */
116
+ ...(ngDevMode ? [{ debugName: "delayDuration" }] : /* istanbul ignore next */ []));
117
+ /**
118
+ * ms before an item closes after hover leaves. Default `150`. The default
119
+ * is read from `provideForNavigationMenuDefaults` for the surrounding
120
+ * scope.
121
+ */
122
+ closeDelay = input(this.#defaults.closeDelay, /* @ts-ignore */
123
+ ...(ngDevMode ? [{ debugName: "closeDelay" }] : /* istanbul ignore next */ []));
124
+ /**
125
+ * ms after a peer item closed during which the next open is instant.
126
+ * Keeps fluid hover-across-triggers feeling responsive. Default `300`.
127
+ * The default is read from `provideForNavigationMenuDefaults` for the
128
+ * surrounding scope.
129
+ */
130
+ skipDelayDuration = input(this.#defaults.skipDelayDuration, /* @ts-ignore */
131
+ ...(ngDevMode ? [{ debugName: "skipDelayDuration" }] : /* istanbul ignore next */ []));
132
+ #triggers = new Collection();
133
+ #contents = new Collection();
134
+ #viewport = signal(null, /* @ts-ignore */
135
+ ...(ngDevMode ? [{ debugName: "#viewport" }] : /* istanbul ignore next */ []));
136
+ /**
137
+ * Previously open `value`, tracked so consumers can read the most recent
138
+ * open value before the current one.
139
+ *
140
+ * `linkedSignal` is the canonical replacement for `effect()` writing to a
141
+ * signal: each time `value()` changes, computation runs with the new
142
+ * source plus the prior `{ source, value }` and returns the *previous*
143
+ * source as the new value.
144
+ *
145
+ * Note: `data-motion` no longer derives from a single `previousValue`
146
+ * (it could only remember one leaving panel). Per-panel frozen motion is
147
+ * tracked in `#motion` instead, so several panels can leave at once during
148
+ * overlapping `animate.leave` transitions without losing their direction.
149
+ */
150
+ previousValue = linkedSignal({ ...(ngDevMode ? { debugName: "previousValue" } : /* istanbul ignore next */ {}), source: () => this.value(),
151
+ computation: (_current, prev) => prev?.source ?? '' });
152
+ /**
153
+ * Frozen `data-motion` per mounted panel `value`. Recorded imperatively at
154
+ * each `open()` / `close()` transition — the entering panel and the panel
155
+ * that just started leaving — and left untouched for panels that began
156
+ * leaving in an earlier transition, so their direction stays stable for as
157
+ * long as `animate.leave` keeps them mounted. Cleared when a panel
158
+ * unmounts (`unregisterContent`) or re-enters as the current value.
159
+ *
160
+ * Backed by a `signal` holding an immutable `Map` so `motionFor` stays a
161
+ * pure, pull-based read — no state is propagated from an `effect()`.
162
+ */
163
+ #motion = signal(new Map(), /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "#motion" }] : /* istanbul ignore next */ []));
165
+ /** The value a pending hover-open is queued for (so a same-trigger leave can cancel it). */
166
+ #pendingOpenValue = null;
167
+ #openAction = createDebouncedAction(() => {
168
+ const value = this.#pendingOpenValue;
169
+ this.#pendingOpenValue = null;
170
+ if (value !== null) {
171
+ this.open(value);
172
+ }
173
+ });
174
+ #closeAction = createDebouncedAction(() => this.close());
175
+ #skipDelayTimer = null;
176
+ #skipDelayActive = false;
177
+ #dismiss = injectDismissableLayer();
178
+ constructor() {
179
+ inject(DestroyRef).onDestroy(() => {
180
+ this.#cancelPending();
181
+ this.#cancelSkipDelay();
182
+ this.#dismiss.deactivate();
183
+ });
184
+ }
185
+ isOpen(value) {
186
+ return value !== '' && this.value() === value;
187
+ }
188
+ toggle(value) {
189
+ if (this.disabled())
190
+ return;
191
+ if (this.value() === value) {
192
+ this.close();
193
+ }
194
+ else {
195
+ this.open(value);
196
+ }
197
+ }
198
+ open(value) {
199
+ if (this.disabled() || value === '')
200
+ return;
201
+ this.#cancelPending();
202
+ if (this.value() === value)
203
+ return;
204
+ this.#recordMotion(this.value(), value);
205
+ this.value.set(value);
206
+ this.#dismiss.activate({
207
+ onEscapeKeyDown: () => {
208
+ const current = this.value();
209
+ this.close();
210
+ const trigger = current ? this.triggerHostFor(current) : null;
211
+ trigger?.focus();
212
+ },
213
+ onPointerDownOutside: () => this.close(),
214
+ });
215
+ }
216
+ close() {
217
+ this.#cancelPending();
218
+ if (this.value() === '')
219
+ return;
220
+ this.#recordMotion(this.value(), '');
221
+ this.value.set('');
222
+ this.#dismiss.deactivate();
223
+ this.#startSkipDelay();
224
+ }
225
+ scheduleOpen(value, reason) {
226
+ if (this.disabled() || value === '')
227
+ return;
228
+ this.#closeAction.cancel();
229
+ this.#clearOpenTimer();
230
+ if (this.value() === value)
231
+ return;
232
+ if (reason === 'click' || reason === 'keyboard') {
233
+ this.open(value);
234
+ return;
235
+ }
236
+ this.#pendingOpenValue = value;
237
+ this.#openAction.schedule(this.#skipDelayActive ? 0 : this.delayDuration());
238
+ }
239
+ scheduleClose(reason, value) {
240
+ if (reason === 'click' || reason === 'keyboard') {
241
+ this.#clearOpenTimer();
242
+ this.#closeAction.cancel();
243
+ this.close();
244
+ return;
245
+ }
246
+ if (this.#openAction.isPending()) {
247
+ // A hover-open is queued. If it is for the trigger we are leaving, cancel
248
+ // it so a quick hover-then-leave on a closed trigger doesn't open after
249
+ // the pointer is gone. If it is for a sibling (hover-across), leave it so
250
+ // that pending open takes over.
251
+ if (value !== undefined && this.#pendingOpenValue === value) {
252
+ this.#clearOpenTimer();
253
+ }
254
+ return;
255
+ }
256
+ this.#closeAction.cancel();
257
+ if (this.value() === '')
258
+ return;
259
+ this.#closeAction.schedule(this.closeDelay());
260
+ }
261
+ cancelPending() {
262
+ this.#cancelPending();
263
+ }
264
+ /**
265
+ * APG: moving focus out of the navigation closes any open dropdown. Fires
266
+ * when Tab/Shift+Tab walks past the last/first focusable inside the nav,
267
+ * or when something else steals focus. The dismissable layer already
268
+ * handles Escape and outside pointerdown; this covers the keyboard-tab
269
+ * case the layer can't see.
270
+ */
271
+ onFocusOut(event) {
272
+ if (this.value() === '') {
273
+ return;
274
+ }
275
+ const next = event.relatedTarget;
276
+ // `null` means focus is leaving the document (e.g. browser chrome) or
277
+ // moving to a non-focusable area — both qualify as "outside the nav".
278
+ if (next && this.#host.nativeElement.contains(next)) {
279
+ return;
280
+ }
281
+ this.close();
282
+ }
283
+ navigate(currentTrigger, action) {
284
+ if (this.disabled())
285
+ return;
286
+ const target = nextEnabledHandle(this.#triggers.items(), currentTrigger, action, {
287
+ loop: this.loop(),
288
+ });
289
+ target?.host.focus();
290
+ }
291
+ focusTrigger(value) {
292
+ this.triggerHostFor(value)?.focus();
293
+ }
294
+ registerTrigger(handle) {
295
+ this.#triggers.register(handle);
296
+ }
297
+ unregisterTrigger(handle) {
298
+ this.#triggers.unregister(handle);
299
+ }
300
+ registerContent(handle) {
301
+ this.#contents.register(handle);
302
+ }
303
+ unregisterContent(handle) {
304
+ this.#contents.unregister(handle);
305
+ this.#clearMotion(handle.value());
306
+ }
307
+ registerViewport(handle) {
308
+ this.#viewport.set(handle);
309
+ }
310
+ unregisterViewport(handle) {
311
+ if (this.#viewport() === handle) {
312
+ this.#viewport.set(null);
313
+ }
314
+ }
315
+ viewport = this.#viewport.asReadonly();
316
+ contentIdFor(value) {
317
+ for (const c of this.#contents.items()) {
318
+ if (c.value() === value) {
319
+ return c.id();
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ triggerIdFor(value) {
325
+ for (const t of this.#triggers.items()) {
326
+ if (t.value() === value) {
327
+ return t.id();
328
+ }
329
+ }
330
+ return null;
331
+ }
332
+ triggerHostFor(value) {
333
+ for (const t of this.#triggers.items()) {
334
+ if (t.value() === value) {
335
+ return t.host;
336
+ }
337
+ }
338
+ return null;
339
+ }
340
+ /** Layout-oriented selectors for indicator positioning. */
341
+ activeTriggerHost = computed(() => {
342
+ const v = this.value();
343
+ if (v === '')
344
+ return null;
345
+ for (const t of this.#triggers.items()) {
346
+ if (t.value() === v)
347
+ return t.host;
348
+ }
349
+ return null;
350
+ }, /* @ts-ignore */
351
+ ...(ngDevMode ? [{ debugName: "activeTriggerHost" }] : /* istanbul ignore next */ []));
352
+ /** Currently-active content's host element, if mounted. */
353
+ activeContentHost = computed(() => {
354
+ const v = this.value();
355
+ if (v === '')
356
+ return null;
357
+ for (const c of this.#contents.items()) {
358
+ if (c.value() === v)
359
+ return c.host;
360
+ }
361
+ return null;
362
+ }, /* @ts-ignore */
363
+ ...(ngDevMode ? [{ debugName: "activeContentHost" }] : /* istanbul ignore next */ []));
364
+ /**
365
+ * `data-motion` direction for the content with the given `value`.
366
+ *
367
+ * Sets the slide direction by index comparison: when the user
368
+ * moves rightward from A (index 0) to B (index 2), the entering panel
369
+ * slides in from the end side (`from-end`) and the leaving panel slides
370
+ * out to the start side (`to-start`) — both shift in the same logical
371
+ * direction, like a horizontal carousel advancing.
372
+ *
373
+ * - The currently-entering content (`value === this.value()`) reflects
374
+ * `from-start` / `from-end` based on which side the previous trigger
375
+ * sat on relative to it.
376
+ * - A leaving content reflects the `to-start` / `to-end` direction frozen
377
+ * at the transition that started its exit, based on which side the
378
+ * then-new trigger sat on relative to it. The direction stays stable for
379
+ * as long as the panel is mounted, even across later transitions.
380
+ * - Anything else (and first open / last close, where there is no peer
381
+ * to compare against) returns `null` so the host binding emits no
382
+ * attribute.
383
+ *
384
+ * "Start" and "end" are logical: index 0 in DOM order is "start", which
385
+ * maps to the visual right in `dir="rtl"`. Consumers can author the
386
+ * keyframes once with logical CSS (e.g. `inset-inline-start`) and it
387
+ * flips automatically.
388
+ *
389
+ * The currently-entering panel is computed live against `previousValue()`;
390
+ * every other (leaving) panel reads its frozen direction from `#motion`,
391
+ * recorded at the transition that started its exit. This keeps a leaving
392
+ * panel's direction stable across later, overlapping transitions instead
393
+ * of dropping to `null` once `previousValue` advances past it.
394
+ */
395
+ motionFor(value) {
396
+ if (value === '')
397
+ return null;
398
+ if (value === this.value()) {
399
+ return this.#enterMotion(this.previousValue(), value);
400
+ }
401
+ return this.#motion().get(value) ?? null;
402
+ }
403
+ /**
404
+ * Record the frozen motion for a `from` → `to` transition: the entering
405
+ * `to` panel and the leaving `from` panel. Existing entries for other
406
+ * still-leaving panels are preserved so overlapping exits keep their
407
+ * direction. `to`'s own stale leaving entry is dropped (it is re-entering).
408
+ */
409
+ #recordMotion(from, to) {
410
+ const next = new Map(this.#motion());
411
+ if (to !== '') {
412
+ const enter = this.#enterMotion(from, to);
413
+ if (enter === null)
414
+ next.delete(to);
415
+ else
416
+ next.set(to, enter);
417
+ }
418
+ if (from !== '') {
419
+ const leave = this.#leaveMotion(from, to);
420
+ if (leave === null)
421
+ next.delete(from);
422
+ else
423
+ next.set(from, leave);
424
+ }
425
+ this.#motion.set(next);
426
+ }
427
+ #clearMotion(value) {
428
+ if (!this.#motion().has(value))
429
+ return;
430
+ const next = new Map(this.#motion());
431
+ next.delete(value);
432
+ this.#motion.set(next);
433
+ }
434
+ /** Entering direction for `to` given the panel `from` it replaced. */
435
+ #enterMotion(from, to) {
436
+ if (from === '' || from === to)
437
+ return null;
438
+ const fromIndex = this.#triggerIndexFor(from);
439
+ const toIndex = this.#triggerIndexFor(to);
440
+ if (fromIndex < 0 || toIndex < 0)
441
+ return null;
442
+ if (fromIndex < toIndex)
443
+ return 'from-end';
444
+ if (fromIndex > toIndex)
445
+ return 'from-start';
446
+ return null;
447
+ }
448
+ /** Leaving direction for `from` given the panel `to` that replaced it. */
449
+ #leaveMotion(from, to) {
450
+ if (to === '' || to === from)
451
+ return null;
452
+ const fromIndex = this.#triggerIndexFor(from);
453
+ const toIndex = this.#triggerIndexFor(to);
454
+ if (fromIndex < 0 || toIndex < 0)
455
+ return null;
456
+ if (toIndex > fromIndex)
457
+ return 'to-start';
458
+ if (toIndex < fromIndex)
459
+ return 'to-end';
460
+ return null;
461
+ }
462
+ #triggerIndexFor(value) {
463
+ const triggers = this.#triggers.items();
464
+ for (let i = 0; i < triggers.length; i++) {
465
+ if (triggers[i].value() === value)
466
+ return i;
467
+ }
468
+ return -1;
469
+ }
470
+ #cancelPending() {
471
+ this.#clearOpenTimer();
472
+ this.#closeAction.cancel();
473
+ }
474
+ #clearOpenTimer() {
475
+ this.#openAction.cancel();
476
+ this.#pendingOpenValue = null;
477
+ }
478
+ #startSkipDelay() {
479
+ this.#cancelSkipDelay();
480
+ this.#skipDelayActive = true;
481
+ this.#skipDelayTimer = setTimeout(() => {
482
+ this.#skipDelayTimer = null;
483
+ this.#skipDelayActive = false;
484
+ }, Math.max(0, this.skipDelayDuration()));
485
+ }
486
+ #cancelSkipDelay() {
487
+ if (this.#skipDelayTimer !== null) {
488
+ clearTimeout(this.#skipDelayTimer);
489
+ this.#skipDelayTimer = null;
490
+ }
491
+ this.#skipDelayActive = false;
492
+ }
493
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
494
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForNavigationMenu, isStandalone: true, selector: "[forNavigationMenu]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, delayDuration: { classPropertyName: "delayDuration", publicName: "delayDuration", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null }, skipDelayDuration: { classPropertyName: "skipDelayDuration", publicName: "skipDelayDuration", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.data-state": "value() === \"\" ? \"closed\" : \"open\"", "attr.data-orientation": "orientation()", "attr.data-disabled": "disabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [{ provide: FOR_NAVIGATION_MENU_CONTEXT, useExisting: ForNavigationMenu }], exportAs: ["forNavigationMenu"], ngImport: i0 });
495
+ }
496
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenu, decorators: [{
497
+ type: Directive,
498
+ args: [{
499
+ selector: '[forNavigationMenu]',
500
+ exportAs: 'forNavigationMenu',
501
+ host: {
502
+ '[attr.aria-label]': 'ariaLabel() || null',
503
+ '[attr.data-state]': 'value() === "" ? "closed" : "open"',
504
+ '[attr.data-orientation]': 'orientation()',
505
+ '[attr.data-disabled]': 'disabled() ? "" : null',
506
+ '[attr.dir]': 'dir()',
507
+ '(focusout)': 'onFocusOut($event)',
508
+ },
509
+ providers: [{ provide: FOR_NAVIGATION_MENU_CONTEXT, useExisting: ForNavigationMenu }],
510
+ }]
511
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], delayDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "delayDuration", required: false }] }], closeDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeDelay", required: false }] }], skipDelayDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipDelayDuration", required: false }] }] } });
512
+
513
+ /**
514
+ * Wrapper around the row / column of items. Carries `data-orientation` so
515
+ * the consumer can flip CSS layout. No semantic role — this is a layout
516
+ * helper, not a `menubar` (the disclosure pattern doesn't use one).
517
+ */
518
+ class ForNavigationMenuList {
519
+ ctx = injectNavigationMenuContext('ForNavigationMenuList');
520
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuList, deps: [], target: i0.ɵɵFactoryTarget.Directive });
521
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForNavigationMenuList, isStandalone: true, selector: "[forNavigationMenuList]", host: { properties: { "attr.data-orientation": "ctx.orientation()" } }, exportAs: ["forNavigationMenuList"], ngImport: i0 });
522
+ }
523
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuList, decorators: [{
524
+ type: Directive,
525
+ args: [{
526
+ selector: '[forNavigationMenuList]',
527
+ exportAs: 'forNavigationMenuList',
528
+ host: {
529
+ '[attr.data-orientation]': 'ctx.orientation()',
530
+ },
531
+ }]
532
+ }] });
533
+
534
+ /**
535
+ * Wrapper around one trigger + its content. Owns the item's `value` (the
536
+ * id used to coordinate which item is open) and its disabled state, then
537
+ * exposes them downstream so the trigger and content can share a single
538
+ * source of truth.
539
+ */
540
+ class ForNavigationMenuItem {
541
+ #ctx = injectNavigationMenuContext('ForNavigationMenuItem');
542
+ /** Identifier matched against the menu's `value`. Required. */
543
+ value = input.required(/* @ts-ignore */
544
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
545
+ /** Per-item disabled (in addition to the menu-level `disabled`). */
546
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
547
+ state = computed(() => (this.#ctx.isOpen(this.value()) ? 'open' : 'closed'), /* @ts-ignore */
548
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
549
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
550
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForNavigationMenuItem, isStandalone: true, selector: "[forNavigationMenuItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-state": "state()", "attr.data-disabled": "disabled() ? \"\" : null" } }, providers: [
551
+ {
552
+ provide: FOR_NAVIGATION_MENU_ITEM_CONTEXT,
553
+ useExisting: ForNavigationMenuItem,
554
+ },
555
+ ], exportAs: ["forNavigationMenuItem"], ngImport: i0 });
556
+ }
557
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuItem, decorators: [{
558
+ type: Directive,
559
+ args: [{
560
+ selector: '[forNavigationMenuItem]',
561
+ exportAs: 'forNavigationMenuItem',
562
+ host: {
563
+ '[attr.data-state]': 'state()',
564
+ '[attr.data-disabled]': 'disabled() ? "" : null',
565
+ },
566
+ providers: [
567
+ {
568
+ provide: FOR_NAVIGATION_MENU_ITEM_CONTEXT,
569
+ useExisting: ForNavigationMenuItem,
570
+ },
571
+ ],
572
+ }]
573
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
574
+
575
+ /**
576
+ * Disclosure trigger. Apply on `<button>`. Hover / click open the paired
577
+ * `[forNavigationMenuContent]`; arrow keys move focus across siblings;
578
+ * ArrowDown (horizontal) / ArrowRight (vertical) open; Escape closes;
579
+ * Enter / Space toggle.
580
+ *
581
+ * Per the APG disclosure-navigation pattern this does NOT open on plain
582
+ * focus: Tabbing across the trigger row must not auto-expand panels, and a
583
+ * programmatic return-focus (e.g. after Escape) must not synchronously
584
+ * re-open the panel that just closed.
585
+ */
586
+ class ForNavigationMenuTrigger {
587
+ menu = injectNavigationMenuContext('ForNavigationMenuTrigger');
588
+ #item = injectNavigationMenuItemContext('ForNavigationMenuTrigger');
589
+ #host = inject(ElementRef);
590
+ id = hostId('for-navigation-menu-trigger');
591
+ value = this.#item.value;
592
+ disabled = this.#item.disabled;
593
+ isOpen = computed(() => this.menu.isOpen(this.value()), /* @ts-ignore */
594
+ ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
595
+ contentId = computed(() => this.menu.contentIdFor(this.value()) ?? '', /* @ts-ignore */
596
+ ...(ngDevMode ? [{ debugName: "contentId" }] : /* istanbul ignore next */ []));
597
+ constructor() {
598
+ const handle = {
599
+ host: this.#host.nativeElement,
600
+ value: this.value,
601
+ disabled: this.disabled,
602
+ id: this.id,
603
+ };
604
+ // Defer registration so the parent's `triggerIdFor` / `contentIdFor` /
605
+ // `triggerHostFor` lookups can read `handle.value()` without hitting the
606
+ // not-yet-bound `input.required` throw on the owning `[forNavigationMenuItem]`.
607
+ // `unregisterTrigger` is reference-based, so destroy-before-register is a
608
+ // safe no-op.
609
+ registerHandle(handle, (h) => this.menu.registerTrigger(h), (h) => this.menu.unregisterTrigger(h), 'afterNextRender');
610
+ reflectDisabled(this.disabled);
611
+ }
612
+ onClick() {
613
+ if (this.disabled())
614
+ return;
615
+ this.menu.toggle(this.value());
616
+ }
617
+ onPointerEnter() {
618
+ if (this.disabled())
619
+ return;
620
+ this.menu.scheduleOpen(this.value(), 'hover');
621
+ }
622
+ onPointerLeave() {
623
+ if (this.disabled())
624
+ return;
625
+ this.menu.scheduleClose('hover', this.value());
626
+ }
627
+ onKeyDown(event) {
628
+ if (this.disabled())
629
+ return;
630
+ // Activation: Enter / Space toggle the disclosure.
631
+ if (event.key === 'Enter' || event.key === ' ') {
632
+ event.preventDefault();
633
+ this.menu.toggle(this.value());
634
+ return;
635
+ }
636
+ if (event.key === 'Escape' && this.isOpen()) {
637
+ event.preventDefault();
638
+ event.stopPropagation();
639
+ this.menu.close();
640
+ this.#host.nativeElement.focus();
641
+ return;
642
+ }
643
+ // Cross-axis arrow opens, main-axis arrows navigate.
644
+ const orientation = this.menu.orientation();
645
+ const action = resolveListNavigation(event, {
646
+ orientation,
647
+ dir: this.menu.dir(),
648
+ });
649
+ if (action) {
650
+ event.preventDefault();
651
+ this.menu.navigate(this.#host.nativeElement, action);
652
+ return;
653
+ }
654
+ // ArrowDown (horizontal) / ArrowRight (vertical) opens the disclosure.
655
+ const openKey = orientation === 'horizontal' ? 'ArrowDown' : 'ArrowRight';
656
+ if (event.key === openKey) {
657
+ event.preventDefault();
658
+ this.menu.scheduleOpen(this.value(), 'keyboard');
659
+ }
660
+ }
661
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
662
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForNavigationMenuTrigger, isStandalone: true, selector: "[forNavigationMenuTrigger]", host: { attributes: { "type": "button" }, listeners: { "click": "onClick()", "pointerenter": "onPointerEnter()", "pointerleave": "onPointerLeave()", "keydown": "onKeyDown($event)" }, properties: { "id": "id()", "attr.aria-expanded": "isOpen() ? \"true\" : \"false\"", "attr.aria-controls": "isOpen() ? contentId() : null", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-state": "isOpen() ? \"open\" : \"closed\"", "attr.data-disabled": "disabled() ? \"\" : null" } }, exportAs: ["forNavigationMenuTrigger"], ngImport: i0 });
663
+ }
664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuTrigger, decorators: [{
665
+ type: Directive,
666
+ args: [{
667
+ selector: '[forNavigationMenuTrigger]',
668
+ exportAs: 'forNavigationMenuTrigger',
669
+ host: {
670
+ type: 'button',
671
+ '[id]': 'id()',
672
+ '[attr.aria-expanded]': 'isOpen() ? "true" : "false"',
673
+ '[attr.aria-controls]': 'isOpen() ? contentId() : null',
674
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
675
+ '[attr.data-state]': 'isOpen() ? "open" : "closed"',
676
+ '[attr.data-disabled]': 'disabled() ? "" : null',
677
+ '(click)': 'onClick()',
678
+ '(pointerenter)': 'onPointerEnter()',
679
+ '(pointerleave)': 'onPointerLeave()',
680
+ '(keydown)': 'onKeyDown($event)',
681
+ },
682
+ }]
683
+ }], ctorParameters: () => [] });
684
+
685
+ /**
686
+ * The disclosure panel for a navigation-menu item. Mounted via `@if`
687
+ * (consumer drives it from `forNavigationMenu.value()` or
688
+ * `forNavigationMenuItem`'s state) so animations work natively.
689
+ *
690
+ * Carries `aria-labelledby` pointing at the trigger and reflects
691
+ * `data-state` for CSS hooks. No focus management is imposed — content
692
+ * is a regular landmark; Tab moves through links inside, then out to the
693
+ * next focusable element after the wrapper.
694
+ *
695
+ * When a `[forNavigationMenuViewport]` is present in the menu, the host
696
+ * element re-parents itself into the viewport on mount so all active
697
+ * panels share a single animated surface (mega-menu pattern). The viewport
698
+ * owns the ordering: each panel is inserted in its trigger's document
699
+ * order, never simply appended in mount order. So during an overlapping
700
+ * A→B transition — where the leaving A is still mounted (its
701
+ * `animate.leave` keeps it around) while B enters — the viewport's child
702
+ * order is deterministic and always matches trigger order: the panel whose
703
+ * trigger comes first in the DOM is the first child, irrespective of which
704
+ * panel mounted last. Cross-fade / slide carousels can therefore author
705
+ * their stacking against trigger order (pair it with `data-motion`). The
706
+ * directive does not undo the re-parent on destroy: the consumer's `@if`
707
+ * is destroying the element anyway, and Angular's renderer removes nodes
708
+ * by their actual current parent.
709
+ *
710
+ * `data-motion` is reflected (`from-start | from-end | to-start | to-end`)
711
+ * for CSS keyframes that slide the entering and leaving panels together
712
+ * during a transition.
713
+ */
714
+ class ForNavigationMenuContent {
715
+ menu = injectNavigationMenuContext('ForNavigationMenuContent');
716
+ #item = injectNavigationMenuItemContext('ForNavigationMenuContent');
717
+ #host = inject(ElementRef).nativeElement;
718
+ id = hostId('for-navigation-menu-content');
719
+ value = this.#item.value;
720
+ triggerId = computed(() => this.menu.triggerIdFor(this.value()), /* @ts-ignore */
721
+ ...(ngDevMode ? [{ debugName: "triggerId" }] : /* istanbul ignore next */ []));
722
+ motion = computed(() => this.menu.motionFor(this.value()), /* @ts-ignore */
723
+ ...(ngDevMode ? [{ debugName: "motion" }] : /* istanbul ignore next */ []));
724
+ #mounted = signal(false, /* @ts-ignore */
725
+ ...(ngDevMode ? [{ debugName: "#mounted" }] : /* istanbul ignore next */ []));
726
+ constructor() {
727
+ const handle = {
728
+ host: this.#host,
729
+ value: this.value,
730
+ id: this.id,
731
+ };
732
+ // Defer registration so the parent's lookups can read `handle.value()`
733
+ // without hitting the not-yet-bound `input.required` throw on the owning
734
+ // `[forNavigationMenuItem]`. `unregisterContent` is reference-based, so
735
+ // destroy-before-register is a safe no-op.
736
+ registerHandle(handle, (h) => this.menu.registerContent(h), (h) => this.menu.unregisterContent(h), 'afterNextRender');
737
+ // Defer the re-parent until after the embedded view is attached to its
738
+ // template anchor — Angular inserts root nodes AFTER directive
739
+ // constructors run, so an `appendChild` here would be undone. Running
740
+ // it in `afterNextRender` happens before browser paint, so the move is
741
+ // not visible. The viewport owns ordering: it inserts this panel in its
742
+ // trigger's document order, so an overlapping A→B transition keeps a
743
+ // deterministic child order regardless of which panel mounted last.
744
+ afterNextRender(() => {
745
+ this.#reparent();
746
+ this.#mounted.set(true);
747
+ });
748
+ // Re-parent (and re-order) the panel reactively once mounted.
749
+ // `triggerHostFor` reads the menu's trigger collection, so this re-runs
750
+ // whenever this panel's trigger registers or unregisters — closing the
751
+ // race where the trigger registers AFTER the content (both defer to
752
+ // `afterNextRender`). Until the trigger is known the panel sorts last;
753
+ // once it registers the viewport re-orders it into trigger document
754
+ // order. The re-parent is a DOM side effect on the imperative viewport
755
+ // handle (no signal writes), and it waits for `#mounted` so it never runs
756
+ // before `afterNextRender` has attached the panel under the viewport.
757
+ //
758
+ // Note: the trigger collection is backed by a `MutationObserver`, and this
759
+ // very `appendChild` re-parent mutates the observed subtree, so the
760
+ // collection can re-notify and re-run this effect a second time. That extra
761
+ // pass is EXPECTED and harmless — `insertPanel` / `#reorder` are idempotent
762
+ // (a node already in its target slot is left untouched), so the re-run is a
763
+ // no-op. Do not "fix" the idempotency away to suppress it: the convergence
764
+ // it guarantees is what makes the registration race self-heal.
765
+ effect(() => {
766
+ if (!this.#mounted())
767
+ return;
768
+ this.#reparent();
769
+ });
770
+ }
771
+ #reparent() {
772
+ const viewport = this.menu.viewport();
773
+ if (!viewport)
774
+ return;
775
+ viewport.insertPanel(this.#host, this.menu.triggerHostFor(this.value()));
776
+ }
777
+ onEscape(event) {
778
+ event.preventDefault();
779
+ event.stopPropagation();
780
+ const v = this.value();
781
+ this.menu.close();
782
+ this.menu.focusTrigger(v);
783
+ }
784
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
785
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForNavigationMenuContent, isStandalone: true, selector: "[forNavigationMenuContent]", host: { listeners: { "pointerenter": "menu.cancelPending()", "pointerleave": "menu.scheduleClose(\"hover\")", "keydown.escape": "onEscape($any($event))" }, properties: { "id": "id()", "attr.aria-labelledby": "triggerId()", "attr.data-state": "menu.isOpen(value()) ? \"open\" : \"closed\"", "attr.data-motion": "motion()" } }, exportAs: ["forNavigationMenuContent"], ngImport: i0 });
786
+ }
787
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuContent, decorators: [{
788
+ type: Directive,
789
+ args: [{
790
+ selector: '[forNavigationMenuContent]',
791
+ exportAs: 'forNavigationMenuContent',
792
+ host: {
793
+ '[id]': 'id()',
794
+ '[attr.aria-labelledby]': 'triggerId()',
795
+ '[attr.data-state]': 'menu.isOpen(value()) ? "open" : "closed"',
796
+ '[attr.data-motion]': 'motion()',
797
+ '(pointerenter)': 'menu.cancelPending()',
798
+ '(pointerleave)': 'menu.scheduleClose("hover")',
799
+ '(keydown.escape)': 'onEscape($any($event))',
800
+ },
801
+ }]
802
+ }], ctorParameters: () => [] });
803
+
804
+ /**
805
+ * Decorative marker for links inside a navigation-menu content. Reflects
806
+ * `data-active` for CSS hooks (the consumer flips it based on the
807
+ * router's current URL — the directive doesn't know about routing).
808
+ *
809
+ * Apply on `<a>`.
810
+ */
811
+ class ForNavigationMenuLink {
812
+ /** When true, this link points at the current page. Reflects `aria-current`. */
813
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
814
+ /**
815
+ * Value forwarded to `aria-current` when `active` is true. Defaults to
816
+ * `'page'` (the most common case for top-level navigation).
817
+ */
818
+ ariaCurrent = input('page', /* @ts-ignore */
819
+ ...(ngDevMode ? [{ debugName: "ariaCurrent" }] : /* istanbul ignore next */ []));
820
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
821
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForNavigationMenuLink, isStandalone: true, selector: "[forNavigationMenuLink]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, ariaCurrent: { classPropertyName: "ariaCurrent", publicName: "ariaCurrent", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-active": "active() ? \"\" : null", "attr.aria-current": "active() ? ariaCurrent() : null" } }, exportAs: ["forNavigationMenuLink"], ngImport: i0 });
822
+ }
823
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuLink, decorators: [{
824
+ type: Directive,
825
+ args: [{
826
+ selector: '[forNavigationMenuLink]',
827
+ exportAs: 'forNavigationMenuLink',
828
+ host: {
829
+ '[attr.data-active]': 'active() ? "" : null',
830
+ '[attr.aria-current]': 'active() ? ariaCurrent() : null',
831
+ },
832
+ }]
833
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], ariaCurrent: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaCurrent", required: false }] }] } });
834
+
835
+ /**
836
+ * Optional visual indicator (underline, pill) that follows the active
837
+ * trigger. Computes geometry from the trigger's position relative to
838
+ * `[forNavigationMenuList]` and exposes it via CSS custom properties:
839
+ *
840
+ * - `--for-navigation-menu-indicator-x` — horizontal offset (px)
841
+ * - `--for-navigation-menu-indicator-y` — vertical offset (px)
842
+ * - `--for-navigation-menu-indicator-width` — trigger width (px)
843
+ * - `--for-navigation-menu-indicator-height` — trigger height (px)
844
+ *
845
+ * Drive `transform` / `width` / `height` from these in CSS. The directive
846
+ * is decorative — set `aria-hidden="true"` on the host element if
847
+ * needed.
848
+ *
849
+ * Geometry is re-measured reactively, not on every render: a `ResizeObserver`
850
+ * (browser-only) watches the active trigger and the surrounding list and bumps
851
+ * a measure-tick signal, and the offset / size are recomputed only when the
852
+ * active trigger changes or one of those boxes resizes. `afterNextRender`
853
+ * covers the initial measure once the host is in the DOM.
854
+ */
855
+ class ForNavigationMenuIndicator {
856
+ menu = injectNavigationMenuContext('ForNavigationMenuIndicator');
857
+ #host = inject(ElementRef).nativeElement;
858
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
859
+ #measureTick = signal(0, /* @ts-ignore */
860
+ ...(ngDevMode ? [{ debugName: "#measureTick" }] : /* istanbul ignore next */ []));
861
+ #geometry = computed(() => {
862
+ // Depend on the manual tick so ResizeObserver callbacks invalidate the
863
+ // geometry, and on the active trigger so a trigger switch re-measures.
864
+ this.#measureTick();
865
+ const trigger = this.menu.activeTriggerHost();
866
+ const list = this.#host.parentElement;
867
+ if (!trigger || !list) {
868
+ return { x: 0, y: 0, width: 0, height: 0 };
869
+ }
870
+ const tr = trigger.getBoundingClientRect();
871
+ const lr = list.getBoundingClientRect();
872
+ return { x: tr.left - lr.left, y: tr.top - lr.top, width: tr.width, height: tr.height };
873
+ }, /* @ts-ignore */
874
+ ...(ngDevMode ? [{ debugName: "#geometry" }] : /* istanbul ignore next */ []));
875
+ x = computed(() => this.#geometry().x, /* @ts-ignore */
876
+ ...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
877
+ y = computed(() => this.#geometry().y, /* @ts-ignore */
878
+ ...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
879
+ width = computed(() => this.#geometry().width, /* @ts-ignore */
880
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
881
+ height = computed(() => this.#geometry().height, /* @ts-ignore */
882
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
883
+ state = computed(() => (this.menu.activeTriggerHost() ? 'visible' : 'hidden'), /* @ts-ignore */
884
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
885
+ constructor() {
886
+ // Initial measure once the host (and its list parent) are attached.
887
+ afterNextRender(() => this.#measureTick.update((t) => t + 1));
888
+ if (this.#isBrowser && typeof ResizeObserver !== 'undefined') {
889
+ const ro = new ResizeObserver(() => this.#measureTick.update((t) => t + 1));
890
+ const list = this.#host.parentElement;
891
+ if (list) {
892
+ ro.observe(list);
893
+ }
894
+ // Follow the active trigger: observe it for size changes and re-measure
895
+ // on every switch. Pure side effect on the imperative ResizeObserver — no
896
+ // signal writes from this effect.
897
+ let observed = null;
898
+ effect(() => {
899
+ const active = this.menu.activeTriggerHost();
900
+ if (observed && observed !== active) {
901
+ ro.unobserve(observed);
902
+ observed = null;
903
+ }
904
+ if (active && active !== observed) {
905
+ ro.observe(active);
906
+ observed = active;
907
+ }
908
+ });
909
+ inject(DestroyRef).onDestroy(() => ro.disconnect());
910
+ }
911
+ }
912
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
913
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForNavigationMenuIndicator, isStandalone: true, selector: "[forNavigationMenuIndicator]", host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "state()", "attr.data-orientation": "menu.orientation()", "style.--for-navigation-menu-indicator-x.px": "x()", "style.--for-navigation-menu-indicator-y.px": "y()", "style.--for-navigation-menu-indicator-width.px": "width()", "style.--for-navigation-menu-indicator-height.px": "height()" } }, exportAs: ["forNavigationMenuIndicator"], ngImport: i0 });
914
+ }
915
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuIndicator, decorators: [{
916
+ type: Directive,
917
+ args: [{
918
+ selector: '[forNavigationMenuIndicator]',
919
+ exportAs: 'forNavigationMenuIndicator',
920
+ host: {
921
+ 'aria-hidden': 'true',
922
+ '[attr.data-state]': 'state()',
923
+ '[attr.data-orientation]': 'menu.orientation()',
924
+ '[style.--for-navigation-menu-indicator-x.px]': 'x()',
925
+ '[style.--for-navigation-menu-indicator-y.px]': 'y()',
926
+ '[style.--for-navigation-menu-indicator-width.px]': 'width()',
927
+ '[style.--for-navigation-menu-indicator-height.px]': 'height()',
928
+ },
929
+ }]
930
+ }], ctorParameters: () => [] });
931
+
932
+ /**
933
+ * Optional shared surface for mega-menu style navigation. When present,
934
+ * `[forNavigationMenuContent]` re-parents itself into this host on mount,
935
+ * so all active content panels live inside a single visual container that
936
+ * can animate width/height between trigger groups.
937
+ *
938
+ * The viewport exposes the active content's natural size as CSS custom
939
+ * properties:
940
+ *
941
+ * - `--for-navigation-menu-viewport-width` (px)
942
+ * - `--for-navigation-menu-viewport-height` (px)
943
+ *
944
+ * Drive `width` / `height` from these in CSS to get fluid resize between
945
+ * panels. The directive is a real interactive surface (it hosts the
946
+ * focusable content), so `aria-hidden="false"` is reflected explicitly to
947
+ * counteract any wrapper that defaults to hidden.
948
+ *
949
+ * Two `[forNavigationMenuContent]` panels can briefly co-exist inside the
950
+ * viewport during a transition — the leaving panel stays mounted as long
951
+ * as the consumer's `@if` keeps it around (e.g. `animate.leave`), so the
952
+ * consumer can cross-fade or slide. Mount lifecycle stays with the
953
+ * consumer's template; this directive only re-parents and measures.
954
+ *
955
+ * The viewport owns panel ordering: panels are inserted in their triggers'
956
+ * document order, never simply appended in mount order. So during an
957
+ * overlapping A→B transition the panel whose trigger comes first in the DOM
958
+ * is always the first child of the viewport, regardless of which panel
959
+ * mounted last — giving cross-fade / slide carousels a deterministic axis
960
+ * to author against (pair this with the `data-motion` hook on Content).
961
+ *
962
+ * Measurement always tracks the active panel: the ResizeObserver follows
963
+ * `activeContentHost()`, so a non-active panel kept mounted by
964
+ * `animate.leave` is intentionally no longer measured (its size must not
965
+ * drive the viewport box mid-transition). The exposed
966
+ * `--for-navigation-menu-viewport-*` variables therefore reflect the
967
+ * entering panel as soon as it becomes active.
968
+ *
969
+ * **Avoid a measurement feedback loop.** The exposed
970
+ * `--for-navigation-menu-viewport-width` / `-height` are measured from the
971
+ * *active content panel*, never from the viewport host's own box — but the
972
+ * ResizeObserver does also observe the host (so a surrounding column resize is
973
+ * picked up). Do **not** drive the host's own measured dimension from these
974
+ * variables (e.g. `width: var(--for-navigation-menu-viewport-width)` applied to
975
+ * the viewport host) and then nest the measured panel such that the host's box
976
+ * constrains the panel's: that would let a measure-tick's CSS output feed back
977
+ * into the next observation. Drive `width` / `height` from the variables on a
978
+ * wrapper or transition them on the host without coupling them back to the
979
+ * measured child's size. The measure-tick read path itself is safe — the
980
+ * observer only bumps a counter, and the geometry is read in a `computed`, not
981
+ * written from the same notification.
982
+ *
983
+ * @example
984
+ * ```html
985
+ * <nav forNavigationMenu [(value)]="open">
986
+ * <ul forNavigationMenuList>…</ul>
987
+ * <div forNavigationMenuViewport></div>
988
+ * </nav>
989
+ * ```
990
+ */
991
+ class ForNavigationMenuViewport {
992
+ menu = injectNavigationMenuContext('ForNavigationMenuViewport');
993
+ host = inject(ElementRef).nativeElement;
994
+ #measureTick = signal(0, /* @ts-ignore */
995
+ ...(ngDevMode ? [{ debugName: "#measureTick" }] : /* istanbul ignore next */ []));
996
+ width = computed(() => this.#measureSize().width, /* @ts-ignore */
997
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
998
+ height = computed(() => this.#measureSize().height, /* @ts-ignore */
999
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
1000
+ state = computed(() => (this.menu.value() ? 'open' : 'closed'), /* @ts-ignore */
1001
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
1002
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1003
+ // Tracked panels in insertion order, each with its last known trigger host.
1004
+ // Ordering resolves a trigger's document position from this list rather than
1005
+ // a DOM `aria-labelledby` → `getElementById` round-trip, which silently
1006
+ // returns `null` while a trigger's registration races its content's (both
1007
+ // defer to `afterNextRender`) and degrades ordering to mount order. The
1008
+ // content re-invokes `insertPanel` once its trigger registers, so the list
1009
+ // and the resulting DOM order self-heal. Insertion order is preserved so
1010
+ // panels whose trigger is not yet known keep a stable relative position
1011
+ // (sorted to the end) instead of thrashing as siblings re-insert.
1012
+ #panels = [];
1013
+ constructor() {
1014
+ const handle = {
1015
+ host: this.host,
1016
+ insertPanel: (panel, triggerHost) => this.#insertPanel(panel, triggerHost),
1017
+ };
1018
+ registerHandle(handle, (h) => this.menu.registerViewport(h), (h) => this.menu.unregisterViewport(h));
1019
+ // Track the active content's natural size via ResizeObserver so layout
1020
+ // mutations between renders (e.g. async content load, viewport host
1021
+ // resize from a surrounding column) flow into the exposed CSS variables.
1022
+ // Browser-only API; on SSR / very old runtimes we simply skip the
1023
+ // observation and the `width`/`height` computed signals still settle on
1024
+ // first read because they pull from `getBoundingClientRect` directly.
1025
+ if (this.#isBrowser && typeof ResizeObserver !== 'undefined') {
1026
+ const ro = new ResizeObserver(() => this.#measureTick.update((t) => t + 1));
1027
+ // Always observe the viewport host itself so layout changes that affect
1028
+ // its own box (e.g. the surrounding column resizing) are picked up.
1029
+ ro.observe(this.host);
1030
+ // Switch which content child is being observed whenever the active
1031
+ // panel changes. Pure side effect on the imperative ResizeObserver —
1032
+ // no signal writes from this effect, so the host bindings on
1033
+ // `width`/`height` settle within the same CD pass that the active
1034
+ // content changed in (no CLAUDE.md "state in effects" exception
1035
+ // needed here).
1036
+ let observed = null;
1037
+ effect(() => {
1038
+ const active = this.menu.activeContentHost();
1039
+ if (observed && observed !== active) {
1040
+ ro.unobserve(observed);
1041
+ observed = null;
1042
+ }
1043
+ if (active && active !== observed) {
1044
+ ro.observe(active);
1045
+ observed = active;
1046
+ }
1047
+ });
1048
+ inject(DestroyRef).onDestroy(() => ro.disconnect());
1049
+ }
1050
+ }
1051
+ #insertPanel(panel, triggerHost) {
1052
+ const existing = this.#panels.find((p) => p.panel === panel);
1053
+ if (existing) {
1054
+ existing.triggerHost = triggerHost;
1055
+ }
1056
+ else {
1057
+ this.#panels.push({ panel, triggerHost });
1058
+ }
1059
+ this.#reorder();
1060
+ }
1061
+ // Reconcile the viewport's children to a single deterministic order: panels
1062
+ // whose trigger host is known come first, sorted by the trigger's document
1063
+ // position; panels whose trigger is not yet known follow, in insertion
1064
+ // order. Computing one total order (rather than inserting each panel
1065
+ // relative to the others) makes the result independent of the order in
1066
+ // which panels call `insertPanel`, so a registration race converges instead
1067
+ // of thrashing. Nodes already in their target slot are left untouched, so
1068
+ // re-running this never disturbs an unaffected panel mid-animation.
1069
+ #reorder() {
1070
+ this.#prunePanels();
1071
+ const ordered = [...this.#panels].sort((a, b) => {
1072
+ if (a.triggerHost && b.triggerHost) {
1073
+ if (a.triggerHost === b.triggerHost)
1074
+ return 0;
1075
+ const rel = a.triggerHost.compareDocumentPosition(b.triggerHost);
1076
+ if ((rel & Node.DOCUMENT_POSITION_FOLLOWING) !== 0)
1077
+ return -1;
1078
+ if ((rel & Node.DOCUMENT_POSITION_PRECEDING) !== 0)
1079
+ return 1;
1080
+ return 0;
1081
+ }
1082
+ if (a.triggerHost)
1083
+ return -1;
1084
+ if (b.triggerHost)
1085
+ return 1;
1086
+ return 0;
1087
+ });
1088
+ // Reconcile against the host's live children by index: walk the target
1089
+ // order, and whenever the child currently at position `i` is not the panel
1090
+ // that belongs there, move it in. Moving into the already-correct slot is
1091
+ // a no-op, so an unaffected panel is never disturbed mid-animation. Using
1092
+ // the live child at `i` as the reference (rather than a not-yet-inserted
1093
+ // sibling) keeps every `insertBefore` valid on the first re-parent too.
1094
+ for (let i = 0; i < ordered.length; i++) {
1095
+ const panel = ordered[i].panel;
1096
+ const current = this.host.children[i] ?? null;
1097
+ if (current !== panel) {
1098
+ this.host.insertBefore(panel, current);
1099
+ }
1100
+ }
1101
+ }
1102
+ // Drop panels the consumer's `@if` already destroyed so the tracking list
1103
+ // does not leak detached nodes across mount/unmount cycles. A freshly
1104
+ // mounted panel still sits under its `[forNavigationMenuItem]` (connected,
1105
+ // pending its first re-parent) and must be kept.
1106
+ #prunePanels() {
1107
+ for (let i = this.#panels.length - 1; i >= 0; i--) {
1108
+ if (!this.#panels[i].panel.isConnected) {
1109
+ this.#panels.splice(i, 1);
1110
+ }
1111
+ }
1112
+ }
1113
+ #measureSize() {
1114
+ // Depend on the manual tick so RO callbacks invalidate the computed.
1115
+ this.#measureTick();
1116
+ const active = this.menu.activeContentHost();
1117
+ if (!active)
1118
+ return { width: 0, height: 0 };
1119
+ const r = active.getBoundingClientRect();
1120
+ return { width: r.width, height: r.height };
1121
+ }
1122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuViewport, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1123
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForNavigationMenuViewport, isStandalone: true, selector: "[forNavigationMenuViewport]", host: { attributes: { "aria-hidden": "false" }, properties: { "attr.data-state": "state()", "attr.data-orientation": "menu.orientation()", "style.--for-navigation-menu-viewport-width.px": "width()", "style.--for-navigation-menu-viewport-height.px": "height()" } }, exportAs: ["forNavigationMenuViewport"], ngImport: i0 });
1124
+ }
1125
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForNavigationMenuViewport, decorators: [{
1126
+ type: Directive,
1127
+ args: [{
1128
+ selector: '[forNavigationMenuViewport]',
1129
+ exportAs: 'forNavigationMenuViewport',
1130
+ host: {
1131
+ 'aria-hidden': 'false',
1132
+ '[attr.data-state]': 'state()',
1133
+ '[attr.data-orientation]': 'menu.orientation()',
1134
+ '[style.--for-navigation-menu-viewport-width.px]': 'width()',
1135
+ '[style.--for-navigation-menu-viewport-height.px]': 'height()',
1136
+ },
1137
+ }]
1138
+ }], ctorParameters: () => [] });
1139
+
1140
+ /**
1141
+ * Generated bundle index. Do not edit.
1142
+ */
1143
+
1144
+ export { FOR_NAVIGATION_MENU_CONTEXT, FOR_NAVIGATION_MENU_DEFAULTS, FOR_NAVIGATION_MENU_ITEM_CONTEXT, ForNavigationMenu, ForNavigationMenuContent, ForNavigationMenuIndicator, ForNavigationMenuItem, ForNavigationMenuLink, ForNavigationMenuList, ForNavigationMenuTrigger, ForNavigationMenuViewport, provideForNavigationMenuDefaults };
1145
+ //# sourceMappingURL=forty-cdk-navigation-menu.mjs.map