jfs-components 0.0.62 → 0.0.64

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 (255) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +1 -1
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +1 -1
  4. package/lib/commonjs/components/ActionTile/ActionTile.js +2 -1
  5. package/lib/commonjs/components/AmountInput/AmountInput.js +2 -1
  6. package/lib/commonjs/components/AppBar/AppBar.js +1 -1
  7. package/lib/commonjs/components/Avatar/Avatar.js +184 -162
  8. package/lib/commonjs/components/AvatarGroup/AvatarGroup.js +1 -1
  9. package/lib/commonjs/components/Badge/Badge.js +2 -1
  10. package/lib/commonjs/components/Balance/Balance.js +2 -1
  11. package/lib/commonjs/components/BottomNav/BottomNav.js +2 -1
  12. package/lib/commonjs/components/BottomNavItem/BottomNavItem.js +106 -86
  13. package/lib/commonjs/components/Button/Button.js +190 -93
  14. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  15. package/lib/commonjs/components/Card/Card.js +2 -1
  16. package/lib/commonjs/components/CardCTA/CardCTA.js +1 -1
  17. package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +1 -1
  18. package/lib/commonjs/components/Carousel/Carousel.js +3 -2
  19. package/lib/commonjs/components/Checkbox/Checkbox.js +2 -1
  20. package/lib/commonjs/components/ChipGroup/ChipGroup.js +1 -1
  21. package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
  22. package/lib/commonjs/components/DebitCard/DebitCard.js +1 -1
  23. package/lib/commonjs/components/Disclaimer/Disclaimer.js +2 -1
  24. package/lib/commonjs/components/Divider/Divider.js +2 -1
  25. package/lib/commonjs/components/Drawer/Drawer.js +109 -48
  26. package/lib/commonjs/components/EmptyState/EmptyState.js +2 -1
  27. package/lib/commonjs/components/FilterBar/FilterBar.js +1 -1
  28. package/lib/commonjs/components/Form/Form.js +2 -1
  29. package/lib/commonjs/components/FormField/FormField.js +3 -2
  30. package/lib/commonjs/components/HStack/HStack.js +1 -1
  31. package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -1
  32. package/lib/commonjs/components/IconButton/IconButton.js +118 -128
  33. package/lib/commonjs/components/IconCapsule/IconCapsule.js +61 -57
  34. package/lib/commonjs/components/InputSearch/InputSearch.js +7 -3
  35. package/lib/commonjs/components/LazyList/LazyList.js +1 -1
  36. package/lib/commonjs/components/LinearMeter/LinearMeter.js +3 -2
  37. package/lib/commonjs/components/ListGroup/ListGroup.js +1 -1
  38. package/lib/commonjs/components/ListItem/ListItem.js +190 -142
  39. package/lib/commonjs/components/MediaCard/MediaCard.js +3 -3
  40. package/lib/commonjs/components/MerchantProfile/MerchantProfile.js +2 -1
  41. package/lib/commonjs/components/MoneyValue/MoneyValue.js +2 -1
  42. package/lib/commonjs/components/NavArrow/NavArrow.js +82 -59
  43. package/lib/commonjs/components/NoteInput/NoteInput.js +2 -1
  44. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  45. package/lib/commonjs/components/Numpad/Numpad.js +2 -1
  46. package/lib/commonjs/components/OTP/OTP.js +1 -1
  47. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +2 -1
  48. package/lib/commonjs/components/Popup/Popup.js +2 -1
  49. package/lib/commonjs/components/ProductLabel/ProductLabel.js +2 -1
  50. package/lib/commonjs/components/ProgressBadge/ProgressBadge.js +2 -1
  51. package/lib/commonjs/components/RadioButton/RadioButton.js +2 -1
  52. package/lib/commonjs/components/RechargeCard/RechargeCard.js +2 -1
  53. package/lib/commonjs/components/Screen/Screen.js +1 -1
  54. package/lib/commonjs/components/Section/Section.js +500 -166
  55. package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +3 -2
  56. package/lib/commonjs/components/StatItem/StatItem.js +2 -1
  57. package/lib/commonjs/components/StatusHero/StatusHero.js +2 -1
  58. package/lib/commonjs/components/Stepper/Step.js +2 -1
  59. package/lib/commonjs/components/Stepper/StepLabel.js +2 -1
  60. package/lib/commonjs/components/Stepper/Stepper.js +2 -1
  61. package/lib/commonjs/components/SupportText/SupportText.js +2 -1
  62. package/lib/commonjs/components/SupportText/SupportTextIcon.js +2 -1
  63. package/lib/commonjs/components/SwappableAmount/SwappableAmount.js +2 -1
  64. package/lib/commonjs/components/Tabs/TabItem.js +2 -1
  65. package/lib/commonjs/components/Tabs/Tabs.js +2 -1
  66. package/lib/commonjs/components/Text/Text.js +2 -1
  67. package/lib/commonjs/components/TextInput/TextInput.js +2 -2
  68. package/lib/commonjs/components/ThreadHero/ThreadHero.js +2 -1
  69. package/lib/commonjs/components/Title/Title.js +2 -1
  70. package/lib/commonjs/components/Toast/Toast.js +2 -1
  71. package/lib/commonjs/components/Toggle/Toggle.js +2 -1
  72. package/lib/commonjs/components/Tooltip/Tooltip.js +2 -1
  73. package/lib/commonjs/components/TransactionBubble/TransactionBubble.js +1 -1
  74. package/lib/commonjs/components/TransactionDetails/TransactionDetails.js +2 -2
  75. package/lib/commonjs/components/TransactionStatus/TransactionStatus.js +3 -2
  76. package/lib/commonjs/components/UpiHandle/UpiHandle.js +144 -110
  77. package/lib/commonjs/components/VStack/VStack.js +1 -1
  78. package/lib/commonjs/design-tokens/figma-variables-resolver.js +21 -3
  79. package/lib/commonjs/icons/registry.js +1 -1
  80. package/lib/commonjs/utils/react-utils.js +17 -0
  81. package/lib/module/components/Accordion/Accordion.js +2 -2
  82. package/lib/module/components/ActionFooter/ActionFooter.js +2 -2
  83. package/lib/module/components/ActionTile/ActionTile.js +2 -1
  84. package/lib/module/components/AmountInput/AmountInput.js +2 -1
  85. package/lib/module/components/AppBar/AppBar.js +2 -2
  86. package/lib/module/components/Avatar/Avatar.js +184 -162
  87. package/lib/module/components/AvatarGroup/AvatarGroup.js +2 -2
  88. package/lib/module/components/Badge/Badge.js +2 -1
  89. package/lib/module/components/Balance/Balance.js +2 -1
  90. package/lib/module/components/BottomNav/BottomNav.js +2 -1
  91. package/lib/module/components/BottomNavItem/BottomNavItem.js +108 -88
  92. package/lib/module/components/Button/Button.js +192 -95
  93. package/lib/module/components/ButtonGroup/ButtonGroup.js +2 -2
  94. package/lib/module/components/Card/Card.js +2 -1
  95. package/lib/module/components/CardCTA/CardCTA.js +2 -2
  96. package/lib/module/components/CardProviderInfo/CardProviderInfo.js +2 -2
  97. package/lib/module/components/Carousel/Carousel.js +3 -2
  98. package/lib/module/components/Checkbox/Checkbox.js +2 -1
  99. package/lib/module/components/ChipGroup/ChipGroup.js +2 -2
  100. package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
  101. package/lib/module/components/DebitCard/DebitCard.js +2 -2
  102. package/lib/module/components/Disclaimer/Disclaimer.js +2 -1
  103. package/lib/module/components/Divider/Divider.js +2 -1
  104. package/lib/module/components/Drawer/Drawer.js +109 -48
  105. package/lib/module/components/EmptyState/EmptyState.js +2 -1
  106. package/lib/module/components/FilterBar/FilterBar.js +2 -2
  107. package/lib/module/components/Form/Form.js +2 -1
  108. package/lib/module/components/FormField/FormField.js +3 -2
  109. package/lib/module/components/HStack/HStack.js +2 -2
  110. package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -1
  111. package/lib/module/components/IconButton/IconButton.js +120 -130
  112. package/lib/module/components/IconCapsule/IconCapsule.js +60 -57
  113. package/lib/module/components/InputSearch/InputSearch.js +7 -3
  114. package/lib/module/components/LazyList/LazyList.js +2 -2
  115. package/lib/module/components/LinearMeter/LinearMeter.js +3 -2
  116. package/lib/module/components/ListGroup/ListGroup.js +2 -2
  117. package/lib/module/components/ListItem/ListItem.js +194 -146
  118. package/lib/module/components/MediaCard/MediaCard.js +4 -2
  119. package/lib/module/components/MerchantProfile/MerchantProfile.js +2 -1
  120. package/lib/module/components/MoneyValue/MoneyValue.js +2 -1
  121. package/lib/module/components/NavArrow/NavArrow.js +82 -58
  122. package/lib/module/components/NoteInput/NoteInput.js +2 -1
  123. package/lib/module/components/Nudge/Nudge.js +2 -2
  124. package/lib/module/components/Numpad/Numpad.js +2 -1
  125. package/lib/module/components/OTP/OTP.js +2 -2
  126. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +2 -1
  127. package/lib/module/components/Popup/Popup.js +2 -1
  128. package/lib/module/components/ProductLabel/ProductLabel.js +2 -1
  129. package/lib/module/components/ProgressBadge/ProgressBadge.js +2 -1
  130. package/lib/module/components/RadioButton/RadioButton.js +2 -1
  131. package/lib/module/components/RechargeCard/RechargeCard.js +2 -1
  132. package/lib/module/components/Screen/Screen.js +2 -2
  133. package/lib/module/components/Section/Section.js +503 -169
  134. package/lib/module/components/SegmentedControl/SegmentedControl.js +3 -2
  135. package/lib/module/components/StatItem/StatItem.js +2 -1
  136. package/lib/module/components/StatusHero/StatusHero.js +2 -1
  137. package/lib/module/components/Stepper/Step.js +2 -1
  138. package/lib/module/components/Stepper/StepLabel.js +2 -1
  139. package/lib/module/components/Stepper/Stepper.js +2 -1
  140. package/lib/module/components/SupportText/SupportText.js +2 -1
  141. package/lib/module/components/SupportText/SupportTextIcon.js +2 -1
  142. package/lib/module/components/SwappableAmount/SwappableAmount.js +2 -1
  143. package/lib/module/components/Tabs/TabItem.js +2 -1
  144. package/lib/module/components/Tabs/Tabs.js +2 -1
  145. package/lib/module/components/Text/Text.js +2 -1
  146. package/lib/module/components/TextInput/TextInput.js +3 -3
  147. package/lib/module/components/ThreadHero/ThreadHero.js +2 -1
  148. package/lib/module/components/Title/Title.js +2 -1
  149. package/lib/module/components/Toast/Toast.js +2 -1
  150. package/lib/module/components/Toggle/Toggle.js +2 -1
  151. package/lib/module/components/Tooltip/Tooltip.js +2 -1
  152. package/lib/module/components/TransactionBubble/TransactionBubble.js +2 -2
  153. package/lib/module/components/TransactionDetails/TransactionDetails.js +3 -3
  154. package/lib/module/components/TransactionStatus/TransactionStatus.js +3 -2
  155. package/lib/module/components/UpiHandle/UpiHandle.js +147 -113
  156. package/lib/module/components/VStack/VStack.js +2 -2
  157. package/lib/module/design-tokens/figma-variables-resolver.js +21 -3
  158. package/lib/module/icons/registry.js +1 -1
  159. package/lib/module/utils/react-utils.js +16 -0
  160. package/lib/typescript/src/components/Avatar/Avatar.d.ts +11 -17
  161. package/lib/typescript/src/components/BottomNavItem/BottomNavItem.d.ts +12 -8
  162. package/lib/typescript/src/components/Button/Button.d.ts +18 -1
  163. package/lib/typescript/src/components/IconButton/IconButton.d.ts +12 -29
  164. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +10 -18
  165. package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +8 -3
  166. package/lib/typescript/src/components/ListItem/ListItem.d.ts +14 -1
  167. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +12 -11
  168. package/lib/typescript/src/components/Section/Section.d.ts +43 -48
  169. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +13 -12
  170. package/lib/typescript/src/icons/registry.d.ts +1 -1
  171. package/lib/typescript/src/utils/react-utils.d.ts +15 -0
  172. package/package.json +4 -6
  173. package/src/components/Accordion/Accordion.tsx +2 -2
  174. package/src/components/ActionFooter/ActionFooter.tsx +2 -2
  175. package/src/components/ActionTile/ActionTile.tsx +2 -1
  176. package/src/components/AmountInput/AmountInput.tsx +2 -1
  177. package/src/components/AppBar/AppBar.tsx +2 -2
  178. package/src/components/Avatar/Avatar.tsx +229 -158
  179. package/src/components/AvatarGroup/AvatarGroup.tsx +2 -2
  180. package/src/components/Badge/Badge.tsx +2 -1
  181. package/src/components/Balance/Balance.tsx +2 -1
  182. package/src/components/BottomNav/BottomNav.tsx +2 -1
  183. package/src/components/BottomNavItem/BottomNavItem.tsx +159 -88
  184. package/src/components/Button/Button.tsx +228 -101
  185. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  186. package/src/components/Card/Card.tsx +2 -1
  187. package/src/components/CardCTA/CardCTA.tsx +2 -2
  188. package/src/components/CardProviderInfo/CardProviderInfo.tsx +2 -2
  189. package/src/components/Carousel/Carousel.tsx +3 -2
  190. package/src/components/Checkbox/Checkbox.tsx +2 -1
  191. package/src/components/ChipGroup/ChipGroup.tsx +2 -2
  192. package/src/components/ChipSelect/ChipSelect.tsx +2 -1
  193. package/src/components/DebitCard/DebitCard.tsx +2 -2
  194. package/src/components/Disclaimer/Disclaimer.tsx +2 -1
  195. package/src/components/Divider/Divider.tsx +2 -1
  196. package/src/components/Drawer/Drawer.tsx +124 -58
  197. package/src/components/EmptyState/EmptyState.tsx +2 -1
  198. package/src/components/FilterBar/FilterBar.tsx +2 -2
  199. package/src/components/Form/Form.tsx +2 -1
  200. package/src/components/FormField/FormField.tsx +3 -2
  201. package/src/components/HStack/HStack.tsx +2 -2
  202. package/src/components/HoldingsCard/HoldingsCard.tsx +2 -1
  203. package/src/components/IconButton/IconButton.tsx +154 -126
  204. package/src/components/IconCapsule/IconCapsule.tsx +73 -54
  205. package/src/components/InputSearch/InputSearch.tsx +19 -5
  206. package/src/components/LazyList/LazyList.tsx +2 -2
  207. package/src/components/LinearMeter/LinearMeter.tsx +3 -2
  208. package/src/components/ListGroup/ListGroup.tsx +2 -2
  209. package/src/components/ListItem/ListItem.tsx +257 -187
  210. package/src/components/MediaCard/MediaCard.tsx +2 -1
  211. package/src/components/MerchantProfile/MerchantProfile.tsx +2 -1
  212. package/src/components/MoneyValue/MoneyValue.tsx +2 -1
  213. package/src/components/NavArrow/NavArrow.tsx +91 -58
  214. package/src/components/NoteInput/NoteInput.tsx +2 -1
  215. package/src/components/Nudge/Nudge.tsx +2 -2
  216. package/src/components/Numpad/Numpad.tsx +2 -1
  217. package/src/components/OTP/OTP.tsx +2 -2
  218. package/src/components/PaymentFeedback/PaymentFeedback.tsx +2 -1
  219. package/src/components/Popup/Popup.tsx +2 -1
  220. package/src/components/ProductLabel/ProductLabel.tsx +2 -1
  221. package/src/components/ProgressBadge/ProgressBadge.tsx +2 -2
  222. package/src/components/RadioButton/RadioButton.tsx +2 -1
  223. package/src/components/RechargeCard/RechargeCard.tsx +2 -1
  224. package/src/components/Screen/Screen.tsx +2 -2
  225. package/src/components/Section/Section.tsx +672 -176
  226. package/src/components/SegmentedControl/SegmentedControl.tsx +3 -2
  227. package/src/components/StatItem/StatItem.tsx +2 -1
  228. package/src/components/StatusHero/StatusHero.tsx +2 -1
  229. package/src/components/Stepper/Step.tsx +2 -1
  230. package/src/components/Stepper/StepLabel.tsx +2 -1
  231. package/src/components/Stepper/Stepper.tsx +2 -1
  232. package/src/components/SupportText/SupportText.tsx +2 -1
  233. package/src/components/SupportText/SupportTextIcon.tsx +2 -1
  234. package/src/components/SwappableAmount/SwappableAmount.tsx +2 -1
  235. package/src/components/Tabs/TabItem.tsx +2 -1
  236. package/src/components/Tabs/Tabs.tsx +2 -1
  237. package/src/components/Text/Text.tsx +2 -1
  238. package/src/components/TextInput/TextInput.tsx +3 -3
  239. package/src/components/ThreadHero/ThreadHero.tsx +2 -1
  240. package/src/components/Title/Title.tsx +2 -1
  241. package/src/components/Toast/Toast.tsx +2 -1
  242. package/src/components/Toggle/Toggle.tsx +2 -1
  243. package/src/components/Tooltip/Tooltip.tsx +2 -1
  244. package/src/components/TransactionBubble/TransactionBubble.tsx +2 -2
  245. package/src/components/TransactionDetails/TransactionDetails.tsx +3 -3
  246. package/src/components/TransactionStatus/TransactionStatus.tsx +3 -2
  247. package/src/components/UpiHandle/UpiHandle.tsx +193 -125
  248. package/src/components/VStack/VStack.tsx +2 -2
  249. package/src/design-tokens/figma-variables-resolver.ts +21 -3
  250. package/src/icons/registry.ts +1 -1
  251. package/src/utils/react-utils.ts +16 -0
  252. package/lib/typescript/App.d.ts +0 -2
  253. package/lib/typescript/index.d.ts +0 -2
  254. package/lib/typescript/metro.config.d.ts +0 -78
  255. package/lib/typescript/react-native.config.d.ts +0 -4
@@ -6,83 +6,269 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
+ var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
9
10
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
11
  var _NavArrow = _interopRequireDefault(require("../NavArrow/NavArrow"));
12
+ var _IconCapsule = _interopRequireDefault(require("../IconCapsule/IconCapsule"));
13
+ var _ListItem = _interopRequireDefault(require("../ListItem/ListItem"));
11
14
  var _webPlatformUtils = require("../../utils/web-platform-utils");
12
15
  var _reactUtils = require("../../utils/react-utils");
13
16
  var _jsxRuntime = require("react/jsx-runtime");
14
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
18
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
19
+ // Match Button: delay the press visual on iOS so a scroll-cancelled touch
20
+ // never applies the "pressed" style. See Button.tsx for the rationale.
21
+ const IS_WEB = _reactNative.Platform.OS === 'web';
22
+ const IS_IOS = _reactNative.Platform.OS === 'ios';
23
+ const HEADER_PRESS_DELAY = IS_IOS ? 130 : 0;
24
+
25
+ // Module-scope style constants — never re-allocated per render.
26
+ const headerWrapStyle = {
27
+ flexDirection: 'row',
28
+ alignItems: 'center',
29
+ justifyContent: 'space-between'
30
+ };
31
+ const headerHoverStyle = {
32
+ opacity: 0.95
33
+ };
34
+ const headerPressedStyle = {
35
+ opacity: 0.85
36
+ };
37
+ const headerFocusStyle = {
38
+ borderColor: '#222',
39
+ borderWidth: 1
40
+ };
41
+
16
42
  // ---------------------------------------------------------------------------
17
- // Shared grid layout: measures widest child, enforces uniform width,
18
- // chunks into fixed rows of up to maxColumns, space-between per row.
43
+ // Shared grid layout first-row-anchored sizing.
44
+ //
45
+ // We measure each cell of the *first row* via `onLayout`, take their max as
46
+ // the canonical cellWidth, then apply that explicit width to every cell in
47
+ // every row. Combined with `justify-content: space-between`, this preserves
48
+ // three visual invariants regardless of viewport width:
49
+ // 1. The first cell hugs the container's left edge.
50
+ // 2. The last cell hugs the container's right edge.
51
+ // 3. Cells in row N column K align with cells in row N+1 column K
52
+ // (uniform cell width across the whole grid).
53
+ //
54
+ // Why first-row-anchored?
55
+ //
56
+ // The first row is *always present* (collapsed and expanded both render it),
57
+ // so the measurement happens exactly once on first mount and stays valid for
58
+ // the lifetime of the SlotGrid — the cellWidth is *stable across toggles*.
59
+ // New cells in row 2+ mount when the user expands, and by that time
60
+ // `cellWidth` is already cached, so their `Animated.View` `entering` cascade
61
+ // is never interrupted by a re-measurement-driven re-render.
62
+ //
63
+ // Why not container-width math (e.g. `(containerWidth - gaps) / columns`)?
64
+ // That makes every cell wide enough to fill its share of the row. On wide
65
+ // viewports each cell becomes much wider than its content (icon + label),
66
+ // the content centers inside its oversized cell, and the visible result is
67
+ // a "dead margin" of empty space on the left and right of the grid. Sizing
68
+ // cells to natural content + `space-between` instead pushes the first cell
69
+ // flush left and the last cell flush right, distributing leftover space as
70
+ // the inter-cell gap.
71
+ //
72
+ // Why not measure every cell?
73
+ // The original implementation did, and `max()` could change when the item
74
+ // count changed (collapsed picks one max, expanded another), producing a
75
+ // visible width jump on toggle. The per-cell remeasurement also forced a
76
+ // re-render in the same React batch as the `entering` animation mounting,
77
+ // which Reanimated treats as a cancellation signal — cells visibly didn't
78
+ // animate. Anchoring to the first row eliminates both costs.
79
+ //
80
+ // First-frame behavior is preserved (no blank-flash): until the first-row
81
+ // `onLayout` fires, cells render at their natural width with `space-between`
82
+ // already laying them out correctly; the only thing that changes after
83
+ // measurement is that subsequent rows snap to the same cellWidth so columns
84
+ // align.
19
85
  // ---------------------------------------------------------------------------
20
86
  const SLOT_GRID_MAX_COLUMNS = 4;
21
- function SlotGrid({
87
+
88
+ // Beyond this many "extra" cells, additional cells reuse the cap delay so very
89
+ // large grids (16, 32, …) still feel snappy instead of cascading for >1s.
90
+ const SLOT_GRID_STAGGER_CAP = 8;
91
+ const SLOT_GRID_ENTER_STAGGER_MS = 35;
92
+ const SLOT_GRID_EXIT_STAGGER_MS = 20;
93
+ const SLOT_GRID_EXIT_DURATION_MS = 160;
94
+ const slotGridRowFlowStyle = {
95
+ flexDirection: 'row',
96
+ justifyContent: 'space-between'
97
+ };
98
+ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
22
99
  items,
23
100
  gap,
24
- maxColumns = SLOT_GRID_MAX_COLUMNS
101
+ maxColumns = SLOT_GRID_MAX_COLUMNS,
102
+ animateExtrasFromIndex,
103
+ animateContainerLayout
25
104
  }) {
26
- const [maxItemWidth, setMaxItemWidth] = (0, _react.useState)(null);
27
- const [measureTimedOut, setMeasureTimedOut] = (0, _react.useState)(false);
28
- const itemWidthsRef = (0, _react.useRef)(new Map());
29
105
  const totalItems = items.length;
30
- (0, _react.useEffect)(() => {
31
- itemWidthsRef.current.clear();
32
- setMaxItemWidth(null);
33
- setMeasureTimedOut(false);
34
- }, [totalItems]);
35
- (0, _react.useEffect)(() => {
36
- if (maxItemWidth !== null) return;
37
- const timer = setTimeout(() => setMeasureTimedOut(true), 500);
38
- return () => clearTimeout(timer);
39
- }, [maxItemWidth, totalItems]);
40
- const handleItemLayout = (0, _react.useCallback)((index, width) => {
41
- itemWidthsRef.current.set(index, width);
42
- if (itemWidthsRef.current.size >= totalItems && totalItems > 0) {
43
- setMaxItemWidth(Math.max(...itemWidthsRef.current.values()));
44
- }
45
- }, [totalItems]);
46
- const isMeasured = maxItemWidth !== null;
47
106
  const columns = Math.min(maxColumns, totalItems || 1);
107
+ // Number of cells in the first row. Capped by `columns` (a fully-filled row)
108
+ // and by `totalItems` (e.g. a 1-item grid has a 1-cell first row).
109
+ const firstRowSize = Math.min(columns, totalItems);
110
+
111
+ // First-row width measurement. Only cells whose `itemIndex < firstRowSize`
112
+ // get an `onLayout` callback. Once we have a width sample for each first-row
113
+ // cell, we publish the max and the callbacks become inert — no further
114
+ // measurement happens for the rest of the SlotGrid's lifetime, so toggles
115
+ // never trigger a re-measurement-driven re-render.
116
+ const [firstRowMaxWidth, setFirstRowMaxWidth] = (0, _react.useState)(null);
117
+ const firstRowWidthsRef = (0, _react.useRef)(new Map());
118
+ const handleFirstRowItemLayout = (0, _react.useCallback)((index, width) => {
119
+ const widths = firstRowWidthsRef.current;
120
+ const previous = widths.get(index);
121
+ if (previous !== undefined && Math.abs(previous - width) < 0.5) return;
122
+ widths.set(index, width);
123
+ if (widths.size >= firstRowSize && firstRowSize > 0) {
124
+ let newMax = 0;
125
+ for (const w of widths.values()) {
126
+ if (w > newMax) newMax = w;
127
+ }
128
+ setFirstRowMaxWidth(prev => prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax);
129
+ }
130
+ }, [firstRowSize]);
131
+ const cellWidth = firstRowMaxWidth;
48
132
  const rows = [];
49
133
  for (let i = 0; i < items.length; i += columns) {
50
134
  rows.push(items.slice(i, i + columns));
51
135
  }
52
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
53
- style: {
54
- gap,
55
- ...(isMeasured || measureTimedOut ? {} : {
56
- opacity: 0
57
- })
58
- },
136
+ const containerStyle = (0, _react.useMemo)(() => ({
137
+ gap
138
+ }), [gap]);
139
+ const cellStyle = (0, _react.useMemo)(() => cellWidth !== null ? {
140
+ width: cellWidth
141
+ } : undefined, [cellWidth]);
142
+
143
+ // `space-between` distributes any leftover row space as inter-cell gap, so
144
+ // the first cell is flush-left and the last cell is flush-right regardless
145
+ // of whether cellWidth has been measured yet. Identical layout strategy
146
+ // before and after measurement — no first-frame layout shift.
147
+ const rowStyle = slotGridRowFlowStyle;
148
+ const animationsEnabled = animateExtrasFromIndex !== undefined;
149
+ // Resolve the threshold once. When undefined we treat it as
150
+ // Number.POSITIVE_INFINITY so the per-cell branch always picks the plain
151
+ // `<View>` path.
152
+ const extrasThreshold = animationsEnabled ? animateExtrasFromIndex : Number.POSITIVE_INFINITY;
153
+ // Total count of "extra" cells currently rendered. Used to compute the
154
+ // reverse-stagger delay for the exiting animation so that the last cell
155
+ // leaves first.
156
+ const extrasCount = animationsEnabled ? Math.max(0, totalItems - extrasThreshold) : 0;
157
+ const useAnimatedContainer = animateContainerLayout === true;
158
+
159
+ // Explicit-height clip animation:
160
+ //
161
+ // Reanimated's `LinearTransition` interpolates the container's bounds, and
162
+ // in practice that interpolation drags on the cells inside (they momentarily
163
+ // appear squashed because the parent is shorter than its natural content
164
+ // for the duration of the animation). To keep cells at their *natural size
165
+ // throughout*, we instead:
166
+ // 1. Render the rows inside an inner `<View>` that sizes to its content
167
+ // naturally — cells are never squeezed, never resized.
168
+ // 2. Wrap that inner view in an `Animated.View` with `overflow: 'hidden'`
169
+ // and an explicit `height` driven by a shared value.
170
+ // 3. The inner view reports its natural height via `onLayout`. The first
171
+ // measurement snaps the shared value (no first-mount animation). Every
172
+ // subsequent change (e.g. expand/collapse adds or removes rows) springs
173
+ // the shared value to the new natural height.
174
+ //
175
+ // Visually: the container reveals/conceals content like a curtain, and the
176
+ // cells never deform.
177
+ const animatedHeight = (0, _reactNativeReanimated.useSharedValue)(-1);
178
+ const isFirstHeightLayoutRef = (0, _react.useRef)(true);
179
+ const handleContentLayout = (0, _react.useCallback)(e => {
180
+ const h = e.nativeEvent.layout.height;
181
+ if (h <= 0) return;
182
+ if (isFirstHeightLayoutRef.current) {
183
+ isFirstHeightLayoutRef.current = false;
184
+ animatedHeight.value = h;
185
+ return;
186
+ }
187
+ animatedHeight.value = (0, _reactNativeReanimated.withSpring)(h, {
188
+ damping: 22,
189
+ stiffness: 180,
190
+ reduceMotion: _reactNativeReanimated.ReduceMotion.System
191
+ });
192
+ }, [animatedHeight]);
193
+ const animatedHeightStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => animatedHeight.value < 0 ? {} : {
194
+ height: animatedHeight.value,
195
+ overflow: 'hidden'
196
+ });
197
+ const rowsChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
59
198
  children: rows.map((row, rowIndex) => {
60
199
  const spacersNeeded = row.length < columns ? columns - row.length : 0;
61
200
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
62
- style: {
63
- flexDirection: 'row',
64
- justifyContent: 'space-between'
65
- },
201
+ style: rowStyle,
66
202
  children: [row.map((child, colIndex) => {
67
203
  const itemIndex = rowIndex * columns + colIndex;
204
+ // Only first-row cells participate in measurement, and only
205
+ // until firstRowMaxWidth has been published. After that the
206
+ // onLayout becomes a no-op (we elide it).
207
+ const onLayoutHandler = firstRowMaxWidth === null && itemIndex < firstRowSize ? e => handleFirstRowItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined;
208
+ if (itemIndex >= extrasThreshold) {
209
+ const extraOrdinal = itemIndex - extrasThreshold;
210
+ const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
211
+ const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
212
+ const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
213
+ const entering = _reactNativeReanimated.FadeInUp.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
214
+ const exiting = _reactNativeReanimated.FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
215
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
216
+ entering: entering,
217
+ exiting: exiting,
218
+ ...(onLayoutHandler ? {
219
+ onLayout: onLayoutHandler
220
+ } : null),
221
+ style: cellStyle,
222
+ children: child
223
+ }, itemIndex);
224
+ }
68
225
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
69
- onLayout: !isMeasured ? e => handleItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined,
70
- style: isMeasured ? {
71
- width: maxItemWidth
72
- } : undefined,
226
+ ...(onLayoutHandler ? {
227
+ onLayout: onLayoutHandler
228
+ } : null),
229
+ style: cellStyle,
73
230
  children: child
74
231
  }, itemIndex);
75
- }), isMeasured && spacersNeeded > 0 && Array.from({
232
+ }), cellWidth !== null && spacersNeeded > 0 && Array.from({
76
233
  length: spacersNeeded
77
234
  }, (_, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
78
- style: {
79
- width: maxItemWidth
80
- }
235
+ style: cellStyle
81
236
  }, `spacer-${i}`))]
82
237
  }, rowIndex);
83
238
  })
84
239
  });
240
+ if (useAnimatedContainer) {
241
+ // Outer Animated.View clips and animates height. Inner View holds the
242
+ // rows at natural size and reports its natural height for the spring
243
+ // target. Cell-width measurement happens on the cells themselves
244
+ // (first-row only) — no container-level onLayout is needed.
245
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
246
+ style: animatedHeightStyle,
247
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
248
+ style: containerStyle,
249
+ onLayout: handleContentLayout,
250
+ children: rowsChildren
251
+ })
252
+ });
253
+ }
254
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
255
+ style: containerStyle,
256
+ children: rowsChildren
257
+ });
258
+ }, slotGridPropsAreEqual);
259
+ function slotGridPropsAreEqual(prev, next) {
260
+ if (prev.gap !== next.gap) return false;
261
+ if ((prev.maxColumns ?? SLOT_GRID_MAX_COLUMNS) !== (next.maxColumns ?? SLOT_GRID_MAX_COLUMNS)) return false;
262
+ if (prev.animateExtrasFromIndex !== next.animateExtrasFromIndex) return false;
263
+ if (prev.animateContainerLayout !== next.animateContainerLayout) return false;
264
+ if (prev.items === next.items) return true;
265
+ if (prev.items.length !== next.items.length) return false;
266
+ for (let i = 0; i < prev.items.length; i++) {
267
+ if (prev.items[i] !== next.items[i]) return false;
268
+ }
269
+ return true;
85
270
  }
271
+
86
272
  /**
87
273
  * Section component that mirrors the Figma "Section" component.
88
274
  *
@@ -107,96 +293,99 @@ function SlotGrid({
107
293
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section. If not provided, uses title
108
294
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
109
295
  */
110
- function Section({
111
- title = 'Section title',
112
- supportText = 'Section support text',
113
- showSupportText = true,
114
- slot,
115
- slotDirection = 'row',
116
- modes = {},
117
- onPress,
118
- style,
119
- accessibilityLabel,
120
- accessibilityHint,
121
- webAccessibilityProps,
122
- ...rest
123
- }) {
124
- const [isHeaderFocused, setIsHeaderFocused] = (0, _react.useState)(false);
125
- const [isHeaderHovered, setIsHeaderHovered] = (0, _react.useState)(false);
126
- const [isHeaderPressed, setIsHeaderPressed] = (0, _react.useState)(false);
127
- const headerFocusStyle = isHeaderFocused ? {
128
- borderColor: '#222',
129
- borderWidth: 1
130
- } : {};
131
- const headerHoverStyle = isHeaderHovered ? {
132
- opacity: 0.95
133
- } : {};
134
- const headerPressedStyle = isHeaderPressed ? {
135
- opacity: 0.85
136
- } : {};
137
- // Resolve section container tokens
296
+
297
+ function resolveSectionTokens(modes) {
138
298
  const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('section/background/color', modes) || '#ffffff';
139
299
  const sectionGap = (0, _figmaVariablesResolver.getVariableByName)('section/gap', modes) || 12;
140
300
  const slotGap = (0, _figmaVariablesResolver.getVariableByName)('slot/gap', modes) || 12;
141
301
  const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('section/padding/horizontal', modes) || 12;
142
302
  const paddingVertical = (0, _figmaVariablesResolver.getVariableByName)('section/padding/vertical', modes) || 16;
143
303
  const radius = (0, _figmaVariablesResolver.getVariableByName)('section/radius', modes) || 12;
144
-
145
- // Resolve section header tokens
146
304
  const headerGap = (0, _figmaVariablesResolver.getVariableByName)('section/header/gap', modes) || 8;
147
305
  const headerPaddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('section/header/padding/horizontal', modes) || 0;
148
306
  const headerPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('section/header/padding/vertical', modes) || 0;
149
-
150
- // Resolve section title tokens
151
307
  const titleColor = (0, _figmaVariablesResolver.getVariableByName)('section/title/color', modes) || '#0f0d0a';
152
308
  const titleFontSize = (0, _figmaVariablesResolver.getVariableByName)('section/title/fontSize', modes) || 18;
153
309
  const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('section/title/lineHeight', modes) || 20;
154
310
  const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('section/title/fontFamily', modes) || 'System';
155
311
  const titleFontWeightRaw = (0, _figmaVariablesResolver.getVariableByName)('section/title/fontWeight', modes) || 800;
156
312
  const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
157
-
158
- // Resolve section support text tokens
159
313
  const supportTextColor = (0, _figmaVariablesResolver.getVariableByName)('section/supportText/color', modes) || '#1f1a14';
160
314
  const supportTextFontSize = (0, _figmaVariablesResolver.getVariableByName)('section/supportText/fontSize', modes) || 14;
161
315
  const supportTextLineHeight = (0, _figmaVariablesResolver.getVariableByName)('section/supportText/lineHeight', modes) || 18;
162
316
  const supportTextFontFamily = (0, _figmaVariablesResolver.getVariableByName)('section/supportText/fontFamily', modes) || 'System';
163
317
  const supportTextFontWeightRaw = (0, _figmaVariablesResolver.getVariableByName)('section/supportText/fontWeight', modes) || 500;
164
318
  const supportTextFontWeight = typeof supportTextFontWeightRaw === 'number' ? supportTextFontWeightRaw.toString() : supportTextFontWeightRaw;
165
- const containerStyle = {
166
- backgroundColor,
167
- paddingHorizontal,
168
- paddingVertical,
169
- borderRadius: radius,
170
- gap: sectionGap
171
- };
172
- const headerStyle = {
173
- paddingHorizontal: headerPaddingHorizontal,
174
- paddingVertical: headerPaddingVertical,
175
- gap: headerGap
176
- };
177
- const headerWrapStyle = {
178
- flexDirection: 'row',
179
- alignItems: 'center',
180
- justifyContent: 'space-between'
181
- };
182
- const titleStyle = {
183
- flex: 1,
184
- color: titleColor,
185
- fontSize: titleFontSize,
186
- lineHeight: titleLineHeight,
187
- fontFamily: titleFontFamily,
188
- fontWeight: titleFontWeight
189
- };
190
- const supportTextStyle = {
191
- color: supportTextColor,
192
- fontSize: supportTextFontSize,
193
- lineHeight: supportTextLineHeight,
194
- fontFamily: supportTextFontFamily,
195
- fontWeight: supportTextFontWeight
319
+ return {
320
+ containerStyle: {
321
+ backgroundColor,
322
+ paddingHorizontal,
323
+ paddingVertical,
324
+ borderRadius: radius,
325
+ gap: sectionGap
326
+ },
327
+ headerStyle: {
328
+ paddingHorizontal: headerPaddingHorizontal,
329
+ paddingVertical: headerPaddingVertical,
330
+ gap: headerGap
331
+ },
332
+ titleStyle: {
333
+ flex: 1,
334
+ color: titleColor,
335
+ fontSize: titleFontSize,
336
+ lineHeight: titleLineHeight,
337
+ fontFamily: titleFontFamily,
338
+ fontWeight: titleFontWeight
339
+ },
340
+ supportTextStyle: {
341
+ color: supportTextColor,
342
+ fontSize: supportTextFontSize,
343
+ lineHeight: supportTextLineHeight,
344
+ fontFamily: supportTextFontFamily,
345
+ fontWeight: supportTextFontWeight
346
+ },
347
+ sectionGap,
348
+ slotGap
196
349
  };
350
+ }
351
+ function Section({
352
+ title = 'Section title',
353
+ supportText = 'Section support text',
354
+ showSupportText = true,
355
+ slot,
356
+ slotDirection = 'row',
357
+ modes = _reactUtils.EMPTY_MODES,
358
+ onPress,
359
+ style,
360
+ // accessibilityLabel is intentionally accepted on the type for API
361
+ // back-compat, but the inner Pressable/View deliberately pass
362
+ // `accessibilityLabel={undefined}` (the title Text carries the label
363
+ // instead). Prefix to satisfy the unused-var lint while keeping the prop
364
+ // shape unchanged.
365
+ accessibilityLabel: _accessibilityLabel,
366
+ accessibilityHint,
367
+ webAccessibilityProps,
368
+ ...rest
369
+ }) {
370
+ // Focus and hover are still mirrored in React because they are visible,
371
+ // sustained states (web-only in practice). The setters are gated so they
372
+ // never fire on native — keeping the component render-free during touch.
373
+ // Press is handled imperatively via the `Pressable` style callback so a
374
+ // scroll-cancelled touch never schedules a React render.
375
+ const [isHeaderFocused, setIsHeaderFocused] = (0, _react.useState)(false);
376
+ const [isHeaderHovered, setIsHeaderHovered] = (0, _react.useState)(false);
197
377
 
198
- // Generate default accessibility label from title and supportText
199
- const defaultAccessibilityLabel = accessibilityLabel || (showSupportText ? `${title}. ${supportText}` : title);
378
+ // Mirror user handlers in a ref so our wrappers can stay referentially
379
+ // stable. Without this, every parent re-render would hand Pressable fresh
380
+ // function identities and re-bind every event.
381
+ const userHandlersRef = (0, _react.useRef)({});
382
+ userHandlersRef.current.onPressIn = rest?.onPressIn;
383
+ userHandlersRef.current.onPressOut = rest?.onPressOut;
384
+ userHandlersRef.current.onHoverIn = rest?.onHoverIn;
385
+ userHandlersRef.current.onHoverOut = rest?.onHoverOut;
386
+ userHandlersRef.current.onFocus = rest?.onFocus;
387
+ userHandlersRef.current.onBlur = rest?.onBlur;
388
+ const tokens = (0, _react.useMemo)(() => resolveSectionTokens(modes), [modes]);
200
389
 
201
390
  // Get web platform support props (only used when onPress is defined)
202
391
  const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
@@ -210,7 +399,7 @@ function Section({
210
399
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
211
400
  style: headerWrapStyle,
212
401
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
213
- style: titleStyle,
402
+ style: tokens.titleStyle,
214
403
  numberOfLines: 1,
215
404
  accessibilityElementsHidden: true,
216
405
  importantForAccessibility: "no",
@@ -220,16 +409,49 @@ function Section({
220
409
  modes: modes
221
410
  })]
222
411
  }), showSupportText && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
223
- style: supportTextStyle,
412
+ style: tokens.supportTextStyle,
224
413
  numberOfLines: 2,
225
414
  accessibilityElementsHidden: true,
226
415
  importantForAccessibility: "no",
227
416
  children: supportText
228
417
  })]
229
418
  });
419
+
420
+ // Stable handler identities. User handlers are read through the ref so
421
+ // these wrappers don't need new identities each render.
422
+ const handlePressIn = (0, _react.useCallback)(e => {
423
+ userHandlersRef.current.onPressIn?.(e);
424
+ }, []);
425
+ const handlePressOut = (0, _react.useCallback)(e => {
426
+ userHandlersRef.current.onPressOut?.(e);
427
+ }, []);
428
+ const handleHoverIn = (0, _react.useCallback)(e => {
429
+ if (IS_WEB) setIsHeaderHovered(true);
430
+ userHandlersRef.current.onHoverIn?.(e);
431
+ }, []);
432
+ const handleHoverOut = (0, _react.useCallback)(e => {
433
+ if (IS_WEB) setIsHeaderHovered(false);
434
+ userHandlersRef.current.onHoverOut?.(e);
435
+ }, []);
436
+ const handleFocus = (0, _react.useCallback)(e => {
437
+ if (IS_WEB) setIsHeaderFocused(true);
438
+ userHandlersRef.current.onFocus?.(e);
439
+ }, []);
440
+ const handleBlur = (0, _react.useCallback)(e => {
441
+ if (IS_WEB) setIsHeaderFocused(false);
442
+ userHandlersRef.current.onBlur?.(e);
443
+ }, []);
444
+
445
+ // The pressed visual is applied by the host view directly through the
446
+ // Pressable style callback — no React render is scheduled. We still want
447
+ // the (constant) hover style on web so we keep it in the array.
448
+ const headerStyleCallback = (0, _react.useCallback)(({
449
+ pressed
450
+ }) => [tokens.headerStyle, pressed ? headerPressedStyle : null, isHeaderHovered ? headerHoverStyle : null, isHeaderFocused ? headerFocusStyle : null], [tokens.headerStyle, isHeaderHovered, isHeaderFocused]);
451
+ const containerStyleArray = (0, _react.useMemo)(() => [tokens.containerStyle, style], [tokens.containerStyle, style]);
230
452
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
231
- style: [containerStyle, style],
232
- ...(_reactNative.Platform.OS === 'web' ? {
453
+ style: containerStyleArray,
454
+ ...(IS_WEB ? {
233
455
  accessibilityRole: 'region'
234
456
  } : undefined),
235
457
  accessibilityLabel: undefined,
@@ -238,52 +460,59 @@ function Section({
238
460
  children: [onPress ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
239
461
  accessibilityRole: "button",
240
462
  accessibilityLabel: undefined,
241
- accessibilityHint: accessibilityHint || "Opens section details",
463
+ accessibilityHint: accessibilityHint || 'Opens section details',
242
464
  onPress: onPress,
243
- onPressIn: e => {
244
- setIsHeaderPressed(true);
245
- rest?.onPressIn?.(e);
246
- },
247
- onPressOut: e => {
248
- setIsHeaderPressed(false);
249
- rest?.onPressOut?.(e);
250
- },
251
- onFocus: e => {
252
- setIsHeaderFocused(true);
253
- rest?.onFocus?.(e);
254
- },
255
- onBlur: e => {
256
- setIsHeaderFocused(false);
257
- rest?.onBlur?.(e);
258
- },
259
- onHoverIn: e => {
260
- setIsHeaderHovered(true);
261
- rest?.onHoverIn?.(e);
262
- },
263
- onHoverOut: e => {
264
- setIsHeaderHovered(false);
265
- rest?.onHoverOut?.(e);
266
- },
267
- style: ({
268
- pressed
269
- }) => [headerStyle, pressed ? headerPressedStyle : null, headerHoverStyle, headerFocusStyle],
465
+ onPressIn: handlePressIn,
466
+ onPressOut: handlePressOut,
467
+ onFocus: handleFocus,
468
+ onBlur: handleBlur,
469
+ onHoverIn: handleHoverIn,
470
+ onHoverOut: handleHoverOut,
471
+ unstable_pressDelay: HEADER_PRESS_DELAY,
472
+ style: headerStyleCallback,
270
473
  ...webProps,
271
474
  children: headerContent
272
475
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
273
- style: headerStyle,
476
+ style: tokens.headerStyle,
274
477
  children: headerContent
275
- }), slot && slotDirection === 'row' && /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
276
- items: (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(slot), modes),
277
- gap: sectionGap
278
- }), slot && slotDirection === 'column' && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
279
- style: {
280
- flexDirection: 'column',
281
- gap: slotGap
282
- },
283
- children: (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(slot), modes)
478
+ }), slot && /*#__PURE__*/(0, _jsxRuntime.jsx)(SectionSlot, {
479
+ slot: slot,
480
+ modes: modes,
481
+ direction: slotDirection,
482
+ rowGap: tokens.sectionGap,
483
+ columnGap: tokens.slotGap
284
484
  })]
285
485
  });
286
486
  }
487
+ /**
488
+ * Internal helper that processes the slot children once per (slot, modes) pair
489
+ * and dispatches to the row (SlotGrid) or column layout. Splitting this out of
490
+ * `Section` lets the parent re-render (e.g. for header press/hover state)
491
+ * without re-walking the slot tree via `cloneChildrenWithModes`.
492
+ */
493
+ function SectionSlot({
494
+ slot,
495
+ modes,
496
+ direction,
497
+ rowGap,
498
+ columnGap
499
+ }) {
500
+ const processed = (0, _react.useMemo)(() => (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(slot), modes), [slot, modes]);
501
+ const columnContainerStyle = (0, _react.useMemo)(() => ({
502
+ flexDirection: 'column',
503
+ gap: columnGap
504
+ }), [columnGap]);
505
+ if (direction === 'row') {
506
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
507
+ items: processed,
508
+ gap: rowGap
509
+ });
510
+ }
511
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
512
+ style: columnContainerStyle,
513
+ children: processed
514
+ });
515
+ }
287
516
  /**
288
517
  * Section.Bento component that mirrors the Figma "Section.Bento" component.
289
518
  *
@@ -306,30 +535,88 @@ function Section({
306
535
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section
307
536
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
308
537
  */
538
+ const sectionBentoUpiRowStyle = {
539
+ flexDirection: 'row',
540
+ gap: 8
541
+ };
309
542
  function SectionBento({
310
543
  navSlot,
311
544
  upiSlot,
312
- modes = {},
545
+ modes = _reactUtils.EMPTY_MODES,
313
546
  style,
314
- accessibilityLabel = undefined,
547
+ // Same rationale as Section: accepted on the type but unused internally.
548
+ accessibilityLabel: _accessibilityLabel,
315
549
  accessibilityHint,
550
+ collapsedCount = SLOT_GRID_MAX_COLUMNS,
551
+ defaultExpanded = false,
552
+ expanded: controlledExpanded,
553
+ onExpandedChange,
554
+ toggleMoreLabel = 'More',
555
+ toggleLessLabel = 'Less',
556
+ toggleMoreIcon = 'ic_chevron_down',
557
+ toggleLessIcon = 'ic_chevron_up',
558
+ renderToggle,
316
559
  ...rest
317
560
  }) {
318
- // Resolve section container tokens
319
561
  const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('section/background/color', modes) || '#ffffff';
320
562
  const gap = (0, _figmaVariablesResolver.getVariableByName)('section/gap', modes) || 12;
321
563
  const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('section/padding/horizontal', modes) || 12;
322
564
  const paddingVertical = (0, _figmaVariablesResolver.getVariableByName)('section/padding/vertical', modes) || 16;
323
565
  const radius = (0, _figmaVariablesResolver.getVariableByName)('section/radius', modes) || 12;
324
- const containerStyle = {
566
+ const containerStyle = (0, _react.useMemo)(() => ({
325
567
  backgroundColor,
326
568
  paddingHorizontal,
327
569
  paddingVertical,
328
570
  borderRadius: radius,
329
571
  gap
330
- };
331
- const processedNavSlot = navSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(navSlot), modes) : null;
332
- const processedUpiSlot = upiSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(upiSlot), modes) : null;
572
+ }), [backgroundColor, paddingHorizontal, paddingVertical, radius, gap]);
573
+ const processedNavSlot = (0, _react.useMemo)(() => navSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(navSlot), modes) : null, [navSlot, modes]);
574
+ const processedUpiSlot = (0, _react.useMemo)(() => upiSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(upiSlot), modes) : null, [upiSlot, modes]);
575
+
576
+ // `canExpand` is true iff there are strictly more real items than fit into
577
+ // the collapsed grid. When `allRealItems.length === collapsedCount` we just
578
+ // render them all with no toggle — identical to the pre-feature behavior.
579
+ const allRealItems = (0, _react.useMemo)(() => processedNavSlot ?? [], [processedNavSlot]);
580
+ const canExpand = allRealItems.length > collapsedCount;
581
+ const isControlled = controlledExpanded !== undefined;
582
+ const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
583
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
584
+
585
+ // Mirror onExpandedChange in a ref so `toggle` can stay referentially stable.
586
+ const onExpandedChangeRef = (0, _react.useRef)(onExpandedChange);
587
+ onExpandedChangeRef.current = onExpandedChange;
588
+ const isControlledRef = (0, _react.useRef)(isControlled);
589
+ isControlledRef.current = isControlled;
590
+ const expandedRef = (0, _react.useRef)(expanded);
591
+ expandedRef.current = expanded;
592
+ const toggle = (0, _react.useCallback)(() => {
593
+ const next = !expandedRef.current;
594
+ if (!isControlledRef.current) {
595
+ setInternalExpanded(next);
596
+ }
597
+ onExpandedChangeRef.current?.(next);
598
+ }, []);
599
+ const navGridItems = (0, _react.useMemo)(() => {
600
+ if (!canExpand) {
601
+ return allRealItems;
602
+ }
603
+ // Leave the last collapsed slot for the toggle cell.
604
+ const visibleRealItems = expanded ? allRealItems : allRealItems.slice(0, collapsedCount - 1);
605
+ const toggleNode = renderToggle ? renderToggle({
606
+ expanded,
607
+ toggle
608
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(DefaultBentoToggle, {
609
+ expanded: expanded,
610
+ onPress: toggle,
611
+ modes: modes,
612
+ moreLabel: toggleMoreLabel,
613
+ lessLabel: toggleLessLabel,
614
+ moreIcon: toggleMoreIcon,
615
+ lessIcon: toggleLessIcon,
616
+ extraCount: allRealItems.length - (collapsedCount - 1)
617
+ });
618
+ return [...visibleRealItems, toggleNode];
619
+ }, [canExpand, allRealItems, expanded, collapsedCount, renderToggle, toggle, modes, toggleMoreLabel, toggleLessLabel, toggleMoreIcon, toggleLessIcon]);
333
620
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
334
621
  style: [containerStyle, style],
335
622
  ...(_reactNative.Platform.OS === 'web' ? {
@@ -338,19 +625,66 @@ function SectionBento({
338
625
  accessibilityLabel: undefined,
339
626
  accessibilityHint: accessibilityHint,
340
627
  ...rest,
341
- children: [processedNavSlot && /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
342
- items: processedNavSlot,
343
- gap: gap
628
+ children: [navGridItems.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
629
+ items: navGridItems,
630
+ gap: gap,
631
+ ...(canExpand ? {
632
+ animateExtrasFromIndex: collapsedCount,
633
+ animateContainerLayout: true
634
+ } : null)
344
635
  }), processedUpiSlot && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
345
- style: {
346
- flexDirection: 'row',
347
- gap: 8
348
- },
636
+ style: sectionBentoUpiRowStyle,
349
637
  children: processedUpiSlot
350
638
  })]
351
639
  });
352
640
  }
353
641
 
642
+ // ---------------------------------------------------------------------------
643
+ // DefaultBentoToggle — internal toggle cell rendered by `SectionBento` when no
644
+ // `renderToggle` prop is provided. Uses a vertical `ListItem` so the cell
645
+ // matches the visual rhythm of the surrounding nav items in every mode.
646
+ //
647
+ // Two icons (`ic_chevron_down` / `ic_chevron_up`) are used instead of a
648
+ // rotating single icon, because the toggle cell is reconciled by position
649
+ // (collapsed: end of row 1; expanded: end of last row), so any persistent
650
+ // shared-value-driven rotation would lose its anchor across toggles.
651
+ // ---------------------------------------------------------------------------
652
+
653
+ function DefaultBentoToggle({
654
+ expanded,
655
+ onPress,
656
+ modes,
657
+ moreLabel,
658
+ lessLabel,
659
+ moreIcon,
660
+ lessIcon,
661
+ extraCount
662
+ }) {
663
+ const label = expanded ? lessLabel : moreLabel;
664
+ const iconName = expanded ? lessIcon : moreIcon;
665
+ const accessibilityState = (0, _react.useMemo)(() => ({
666
+ expanded
667
+ }), [expanded]);
668
+ const webAccessibilityProps = (0, _react.useMemo)(() => ({
669
+ ariaExpanded: expanded
670
+ }), [expanded]);
671
+ const accessibilityHint = expanded ? `Hides ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}` : `Shows ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}`;
672
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ListItem.default, {
673
+ layout: "Vertical",
674
+ supportText: label,
675
+ leading: /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
676
+ iconName: iconName,
677
+ modes: modes
678
+ }),
679
+ modes: modes,
680
+ onPress: onPress,
681
+ accessibilityLabel: label,
682
+ accessibilityHint: accessibilityHint,
683
+ accessibilityState: accessibilityState,
684
+ webAccessibilityProps: webAccessibilityProps
685
+ });
686
+ }
687
+
354
688
  // Attach Bento as a property of Section using namespace pattern
355
689
  Section.Bento = SectionBento;
356
690
  var _default = exports.default = Section;