@delacour/react-native-ui 0.1.0-alpha.20260925053522

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 (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +193 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +434 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +242 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +112 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +47 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +95 -0
  108. package/src/components/field/field.tsx +157 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +240 -0
  111. package/src/components/field/field.variants.ts +156 -0
  112. package/src/components/field/index.ts +22 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +74 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +261 -0
  146. package/src/components/pressable/pressable.variants.test.ts +128 -0
  147. package/src/components/pressable/pressable.variants.ts +80 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +245 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +580 -0
  159. package/src/components/radio/radio.variants.ts +271 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +50 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +193 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +856 -0
  202. package/src/components/slider/slider.variants.ts +661 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +287 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +670 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,261 @@
1
+ import {
2
+ Children,
3
+ type ComponentRef,
4
+ type ComponentType,
5
+ createElement,
6
+ isValidElement,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ type Ref,
10
+ useMemo,
11
+ } from "react";
12
+ import type { AccessibilityState, ViewProps } from "react-native";
13
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
14
+ import { Presets } from "react-native-pulsar";
15
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated";
16
+ import { scheduleOnRN } from "react-native-worklets";
17
+ import { useResolveClassNames } from "uniwind";
18
+ import { composeRefs } from "../../lib/compose-refs";
19
+ import { mergeProps } from "../../lib/merge-props";
20
+ import { type PressableFeedback, resolvePressedState, resolveRestOpacity } from "./pressable.variants";
21
+
22
+ const PRESS_SPRING = { damping: 18, mass: 0.4, stiffness: 320 } as const;
23
+
24
+ /**
25
+ * Animated counterpart of each component type seen by `asChild`.
26
+ *
27
+ * `Animated.createAnimatedComponent` must be called once per type, never per
28
+ * render — a fresh wrapper each render remounts the subtree.
29
+ */
30
+ const animatedTypeCache = new WeakMap<object, ComponentType<Record<string, unknown>>>();
31
+
32
+ function resolveAnimatedType(type: unknown): ComponentType<Record<string, unknown>> {
33
+ if (typeof type !== "function" && typeof type !== "object") {
34
+ throw new Error("Pressable asChild expects a component element, not an intrinsic or text node.");
35
+ }
36
+
37
+ const key = type as object;
38
+ const cached = animatedTypeCache.get(key);
39
+ if (cached) return cached;
40
+
41
+ const created = Animated.createAnimatedComponent(type as ComponentType<Record<string, unknown>>) as ComponentType<
42
+ Record<string, unknown>
43
+ >;
44
+ animatedTypeCache.set(key, created);
45
+ return created;
46
+ }
47
+
48
+ export type HapticFeedback = "selection" | "light" | "medium" | "heavy" | "success" | "warning" | "error";
49
+
50
+ /**
51
+ * Fires a haptic preset from the UI thread.
52
+ *
53
+ * Every branch is a worklet, so this runs inside the gesture callback itself —
54
+ * the tap and the haptic land in the same frame rather than waiting on a round
55
+ * trip to the JS thread.
56
+ *
57
+ * Exported because it is the library's *only* haptic vocabulary, and a second
58
+ * component with a gesture of its own — `Slider`, whose pan ticks as the value
59
+ * crosses a step — must reach this switch rather than fork one. It is safe to
60
+ * import across a component folder: nothing in `pressable/` imports a component
61
+ * that could import it back, so there is no cycle for Metro to serve half of.
62
+ */
63
+ export function playHaptic(feedback: HapticFeedback): void {
64
+ "worklet";
65
+ switch (feedback) {
66
+ case "selection":
67
+ Presets.System.selection();
68
+ return;
69
+ case "light":
70
+ Presets.System.impactLight();
71
+ return;
72
+ case "medium":
73
+ Presets.System.impactMedium();
74
+ return;
75
+ case "heavy":
76
+ Presets.System.impactHeavy();
77
+ return;
78
+ case "success":
79
+ Presets.System.notificationSuccess();
80
+ return;
81
+ case "warning":
82
+ Presets.System.notificationWarning();
83
+ return;
84
+ case "error":
85
+ Presets.System.notificationError();
86
+ return;
87
+ default:
88
+ return;
89
+ }
90
+ }
91
+
92
+ export type PressableProps = Omit<ViewProps, "style"> & {
93
+ children?: ReactNode;
94
+ className?: string;
95
+ disabled?: boolean;
96
+ /**
97
+ * Work is in flight: presses are blocked and a screen reader announces the
98
+ * element as busy, but not as disabled. Use this over `disabled` for a
99
+ * temporary state the component clears itself, so assistive tech reports a
100
+ * control that is momentarily unavailable rather than one that is inert.
101
+ */
102
+ busy?: boolean;
103
+ onPress?: () => void;
104
+ onLongPress?: () => void;
105
+ /** Haptic played on press-in. Off by default. */
106
+ haptic?: false | HapticFeedback;
107
+ /**
108
+ * How the press moves the element: `scale`, `fade`, `scale-fade` or `none`.
109
+ * `pressedScale` / `pressedOpacity` still win on the axis they name.
110
+ */
111
+ feedback?: PressableFeedback;
112
+ /** Scale at full press. 1 disables the scale. Overrides `feedback`. */
113
+ pressedScale?: number;
114
+ /** Opacity at full press. 1 disables the fade. Overrides `feedback`. */
115
+ pressedOpacity?: number;
116
+ /** Render into the single child element instead of emitting a View. */
117
+ asChild?: boolean;
118
+ ref?: Ref<ComponentRef<typeof Animated.View>>;
119
+ };
120
+
121
+ /**
122
+ * The interaction primitive every pressable component in this library builds on.
123
+ *
124
+ * Press feedback runs entirely on the UI thread: a Gesture Handler tap drives a
125
+ * shared value, a spring maps it to scale and opacity, and the haptic fires in
126
+ * the same worklet. Only `onPress` and `onLongPress` cross back to JS.
127
+ *
128
+ * How far each axis travels comes from `feedback`, the named vocabulary every
129
+ * pressable in this library shares, or from `pressedScale` / `pressedOpacity`
130
+ * for a value the named modes do not cover.
131
+ *
132
+ * Built on the Gesture API rather than a ready-made pressable so the animation,
133
+ * the haptic and the gesture stay on one thread, and so nothing depends on
134
+ * Gesture Handler's own `Pressable`, which was renamed in its v3.
135
+ */
136
+ export function Pressable({
137
+ children,
138
+ className,
139
+ disabled = false,
140
+ busy = false,
141
+ onPress,
142
+ onLongPress,
143
+ haptic = false,
144
+ feedback,
145
+ pressedScale,
146
+ pressedOpacity,
147
+ asChild = false,
148
+ accessibilityState,
149
+ ref,
150
+ ...props
151
+ }: PressableProps): ReactElement {
152
+ const pressed = useSharedValue(0);
153
+ const interactive = !disabled && !busy;
154
+ // Destructured to primitives on purpose. Reanimated's plugin collects what
155
+ // the worklet closes over, and a fresh object each render would rebuild the
156
+ // animated style every render.
157
+ const { opacity: targetOpacity, scale: targetScale } = resolvePressedState(feedback, pressedScale, pressedOpacity);
158
+ // The animated style below owns `opacity` on the view, so a className's own
159
+ // opacity — a disabled variant's `opacity-50` — is read here and multiplied
160
+ // in rather than overwritten by the resting 1. See `resolveRestOpacity`.
161
+ const restOpacity = resolveRestOpacity(useResolveClassNames(className ?? "").opacity);
162
+
163
+ const gesture = useMemo(() => {
164
+ const tap = Gesture.Tap()
165
+ .enabled(interactive)
166
+ .shouldCancelWhenOutside(true)
167
+ .onBegin(() => {
168
+ "worklet";
169
+ pressed.value = withSpring(1, PRESS_SPRING);
170
+ if (haptic) playHaptic(haptic);
171
+ })
172
+ .onEnd(() => {
173
+ "worklet";
174
+ if (onPress) scheduleOnRN(onPress);
175
+ })
176
+ .onFinalize(() => {
177
+ "worklet";
178
+ pressed.value = withSpring(0, PRESS_SPRING);
179
+ });
180
+
181
+ if (!onLongPress) return tap;
182
+
183
+ const longPress = Gesture.LongPress()
184
+ .enabled(interactive)
185
+ .shouldCancelWhenOutside(true)
186
+ .onStart(() => {
187
+ "worklet";
188
+ scheduleOnRN(onLongPress);
189
+ });
190
+
191
+ return Gesture.Simultaneous(tap, longPress);
192
+ }, [haptic, interactive, onLongPress, onPress, pressed]);
193
+
194
+ const animatedStyle = useAnimatedStyle(() => ({
195
+ opacity: restOpacity * (1 - pressed.value * (1 - targetOpacity)),
196
+ transform: [{ scale: 1 - pressed.value * (1 - targetScale) }],
197
+ }));
198
+
199
+ // Built once and spread at both render sites: a caller-supplied
200
+ // `accessibilityState` merges into it rather than being clobbered by the
201
+ // `{...props}` spread that lands after it.
202
+ const state: AccessibilityState = { ...accessibilityState, busy, disabled };
203
+
204
+ const content = asChild ? (
205
+ renderAsChild(children, {
206
+ accessibilityState: state,
207
+ accessible: true,
208
+ className,
209
+ ref,
210
+ style: animatedStyle,
211
+ ...props,
212
+ })
213
+ ) : (
214
+ <Animated.View
215
+ accessibilityRole="button"
216
+ accessibilityState={state}
217
+ // Without this the view is not an accessibility element on iOS, and the
218
+ // role, state and label above never reach VoiceOver — a composed label
219
+ // on an icon-sized button included. It also merges the children into one
220
+ // element, which is what a control should be.
221
+ accessible
222
+ className={className}
223
+ ref={ref}
224
+ style={animatedStyle}
225
+ {...props}
226
+ >
227
+ {children}
228
+ </Animated.View>
229
+ );
230
+
231
+ return <GestureDetector gesture={gesture}>{content}</GestureDetector>;
232
+ }
233
+ Pressable.displayName = "DelacourUI.Pressable";
234
+
235
+ /**
236
+ * Renders the pressable's props into its single child, with no wrapper element.
237
+ *
238
+ * The child is rendered through the animated counterpart of its own type rather
239
+ * than cloned as-is. A Reanimated style handed to a plain component is deep
240
+ * frozen by React Native in development, and Reanimated's own effect then fails
241
+ * writing to it — so the element receiving the style has to be an animated one.
242
+ */
243
+ function renderAsChild(children: ReactNode, slotProps: Record<string, unknown>): ReactElement {
244
+ const count = Children.count(children);
245
+ if (count !== 1) {
246
+ throw new Error(
247
+ `Pressable asChild expects exactly one child element, received ${count}. Drop \`asChild\` to render a View.`
248
+ );
249
+ }
250
+
251
+ const child = Children.only(children);
252
+ if (!isValidElement(child)) {
253
+ throw new Error("Pressable asChild expects a single React element child; text and fragments cannot take props.");
254
+ }
255
+
256
+ const childProps = child.props as Record<string, unknown> & { ref?: Ref<unknown> };
257
+ const merged = mergeProps<Record<string, unknown>>(slotProps, childProps);
258
+ merged.ref = composeRefs(slotProps.ref as Ref<unknown> | undefined, childProps.ref);
259
+
260
+ return createElement(resolveAnimatedType(child.type), merged);
261
+ }
@@ -0,0 +1,128 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ PRESSABLE_FEEDBACK,
4
+ PRESSABLE_FEEDBACK_FALLBACK,
5
+ PRESSABLE_FEEDBACKS,
6
+ resolvePressedState,
7
+ resolveRestOpacity,
8
+ } from "./pressable.variants";
9
+
10
+ describe("PRESSABLE_FEEDBACK", () => {
11
+ test("covers every named feedback", () => {
12
+ for (const feedback of PRESSABLE_FEEDBACKS) {
13
+ expect(PRESSABLE_FEEDBACK[feedback]).toBeDefined();
14
+ }
15
+ });
16
+
17
+ test("gives every named feedback a distinct pair", () => {
18
+ const seen = new Set(PRESSABLE_FEEDBACKS.map((feedback) => JSON.stringify(PRESSABLE_FEEDBACK[feedback])));
19
+ expect(seen.size).toBe(PRESSABLE_FEEDBACKS.length);
20
+ });
21
+
22
+ // 1 is the neutral value on either axis, so a single-axis mode leaves the
23
+ // other one alone rather than nudging it.
24
+ test("scale shrinks without dimming", () => {
25
+ expect(PRESSABLE_FEEDBACK.scale.opacity).toBe(1);
26
+ expect(PRESSABLE_FEEDBACK.scale.scale).toBeLessThan(1);
27
+ });
28
+
29
+ test("fade dims without scaling", () => {
30
+ expect(PRESSABLE_FEEDBACK.fade.scale).toBe(1);
31
+ expect(PRESSABLE_FEEDBACK.fade.opacity).toBeLessThan(1);
32
+ });
33
+
34
+ test("none moves neither axis", () => {
35
+ expect(PRESSABLE_FEEDBACK.none).toEqual({ opacity: 1, scale: 1 });
36
+ });
37
+
38
+ // The load-bearing one: `scale-fade` is the union of the two single-axis
39
+ // modes, not a third set of numbers. Tuning `scale` or `fade` has to carry
40
+ // through, or the name stops describing what the mode does.
41
+ test("scale-fade takes each axis from the mode that owns it", () => {
42
+ expect(PRESSABLE_FEEDBACK["scale-fade"]).toEqual({
43
+ opacity: PRESSABLE_FEEDBACK.fade.opacity,
44
+ scale: PRESSABLE_FEEDBACK.scale.scale,
45
+ });
46
+ });
47
+
48
+ test("every mode stays within the range the press interpolates over", () => {
49
+ for (const feedback of PRESSABLE_FEEDBACKS) {
50
+ const { opacity, scale } = PRESSABLE_FEEDBACK[feedback];
51
+ expect(opacity).toBeGreaterThan(0);
52
+ expect(opacity).toBeLessThanOrEqual(1);
53
+ expect(scale).toBeGreaterThan(0);
54
+ expect(scale).toBeLessThanOrEqual(1);
55
+ }
56
+ });
57
+ });
58
+
59
+ describe("resolvePressedState", () => {
60
+ test("maps each named feedback to its pair", () => {
61
+ for (const feedback of PRESSABLE_FEEDBACKS) {
62
+ expect(resolvePressedState(feedback, undefined, undefined)).toEqual(PRESSABLE_FEEDBACK[feedback]);
63
+ }
64
+ });
65
+
66
+ // Naming the fallback would either change what a bare Pressable has always
67
+ // done or make `scale-fade` fade less than `fade`. It stays unnamed instead.
68
+ test("falls back to the pair a bare pressable has always used", () => {
69
+ expect(resolvePressedState(undefined, undefined, undefined)).toEqual(PRESSABLE_FEEDBACK_FALLBACK);
70
+ expect(PRESSABLE_FEEDBACK_FALLBACK).toEqual({ opacity: 0.9, scale: 0.97 });
71
+ });
72
+
73
+ test("an explicit value beats the fallback on its own axis", () => {
74
+ expect(resolvePressedState(undefined, 1, undefined)).toEqual({
75
+ opacity: PRESSABLE_FEEDBACK_FALLBACK.opacity,
76
+ scale: 1,
77
+ });
78
+ expect(resolvePressedState(undefined, undefined, 1)).toEqual({
79
+ opacity: 1,
80
+ scale: PRESSABLE_FEEDBACK_FALLBACK.scale,
81
+ });
82
+ });
83
+
84
+ test("an explicit value beats a named feedback on its own axis", () => {
85
+ expect(resolvePressedState("scale-fade", undefined, 0.2)).toEqual({
86
+ opacity: 0.2,
87
+ scale: PRESSABLE_FEEDBACK["scale-fade"].scale,
88
+ });
89
+ expect(resolvePressedState("none", 0.5, undefined)).toEqual({ opacity: 1, scale: 0.5 });
90
+ });
91
+
92
+ test("both explicit values leave nothing of the named feedback", () => {
93
+ expect(resolvePressedState("scale", 0.5, 0.5)).toEqual({ opacity: 0.5, scale: 0.5 });
94
+ });
95
+
96
+ // 0 is a legitimate value on either axis and must not read as "unset".
97
+ test("treats an explicit zero as a value, not an absence", () => {
98
+ expect(resolvePressedState("scale", 0, 0)).toEqual({ opacity: 0, scale: 0 });
99
+ });
100
+ });
101
+
102
+ describe("resolveRestOpacity", () => {
103
+ // The animated style owns `opacity` on the view, so a caller's `opacity-50`
104
+ // would be overwritten by the resting `1` unless the press multiplies it in.
105
+ test("keeps an opacity the className resolved to", () => {
106
+ expect(resolveRestOpacity(0.5)).toBe(0.5);
107
+ expect(resolveRestOpacity(0)).toBe(0);
108
+ expect(resolveRestOpacity(1)).toBe(1);
109
+ });
110
+
111
+ test("falls back to fully opaque when the className sets none", () => {
112
+ expect(resolveRestOpacity(undefined)).toBe(1);
113
+ });
114
+
115
+ // Uniwind types `opacity` as an animatable value, so anything that is not a
116
+ // plain number is treated as unset rather than multiplied in as NaN.
117
+ test("ignores a value that is not a finite number", () => {
118
+ expect(resolveRestOpacity(Number.NaN)).toBe(1);
119
+ expect(resolveRestOpacity(Number.POSITIVE_INFINITY)).toBe(1);
120
+ expect(resolveRestOpacity("0.5")).toBe(1);
121
+ expect(resolveRestOpacity({})).toBe(1);
122
+ });
123
+
124
+ test("clamps a number into the unit range", () => {
125
+ expect(resolveRestOpacity(1.5)).toBe(1);
126
+ expect(resolveRestOpacity(-0.2)).toBe(0);
127
+ });
128
+ });
@@ -0,0 +1,80 @@
1
+ export const PRESSABLE_FEEDBACKS = ["scale", "fade", "scale-fade", "none"] as const;
2
+
3
+ export type PressableFeedback = (typeof PRESSABLE_FEEDBACKS)[number];
4
+
5
+ /** What a press interpolates towards. 1 is the neutral value on either axis. */
6
+ export type PressedState = {
7
+ opacity: number;
8
+ scale: number;
9
+ };
10
+
11
+ /**
12
+ * How a press moves each axis, by name.
13
+ *
14
+ * `scale-fade` is composed from the two single-axis modes rather than given
15
+ * numbers of its own, so tuning `scale` or `fade` carries through to it and the
16
+ * name keeps describing what the mode does.
17
+ *
18
+ * There is no mode involving a ripple, ink or highlight overlay — AGENTS.md
19
+ * rules those out for every pressable in this library.
20
+ *
21
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
22
+ * parse React Native's Flow-typed source. See AGENTS.md.
23
+ */
24
+ export const PRESSABLE_FEEDBACK: Record<PressableFeedback, PressedState> = {
25
+ scale: { opacity: 1, scale: 0.97 },
26
+ fade: { opacity: 0.6, scale: 1 },
27
+ "scale-fade": { opacity: 0.6, scale: 0.97 },
28
+ none: { opacity: 1, scale: 1 },
29
+ };
30
+
31
+ /**
32
+ * The pair a pressable uses with no `feedback` and no explicit values.
33
+ *
34
+ * Deliberately not one of the named modes. It fades less than `fade` does, so
35
+ * naming it would either change what a bare `Pressable` has always done or
36
+ * force `scale-fade` to fade less than `fade` — and a caller who wants a named
37
+ * mode can just say so.
38
+ */
39
+ export const PRESSABLE_FEEDBACK_FALLBACK: PressedState = { opacity: 0.9, scale: 0.97 };
40
+
41
+ /**
42
+ * Folds a named feedback and any explicit values into the pair a press
43
+ * interpolates towards.
44
+ *
45
+ * An explicit `pressedScale` / `pressedOpacity` wins on its own axis and leaves
46
+ * the other one to the named mode, so `feedback="scale-fade"` with
47
+ * `pressedOpacity={0.2}` is a deeper fade at the mode's scale rather than an
48
+ * all-or-nothing choice between the two APIs.
49
+ *
50
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
51
+ */
52
+ export function resolvePressedState(
53
+ feedback: PressableFeedback | undefined,
54
+ pressedScale: number | undefined,
55
+ pressedOpacity: number | undefined
56
+ ): PressedState {
57
+ const named = feedback ? PRESSABLE_FEEDBACK[feedback] : PRESSABLE_FEEDBACK_FALLBACK;
58
+
59
+ return {
60
+ opacity: pressedOpacity ?? named.opacity,
61
+ scale: pressedScale ?? named.scale,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * The opacity a pressable rests at, read from what its `className` resolved to.
67
+ *
68
+ * The press feedback is an animated style, and an animated style owns every
69
+ * key it writes on the view — so a resting `opacity: 1` overwrote a caller's
70
+ * `opacity-50` and a disabled `Button`, `Badge` or `ListGroup.Item` never
71
+ * faded. The press now multiplies this in rather than replacing it. Uniwind
72
+ * types the value as animatable, so anything that is not a finite number is
73
+ * treated as unset, and a number is clamped into `[0, 1]`.
74
+ *
75
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
76
+ */
77
+ export function resolveRestOpacity(resolved: unknown): number {
78
+ if (typeof resolved !== "number" || !Number.isFinite(resolved)) return 1;
79
+ return Math.min(1, Math.max(0, resolved));
80
+ }
@@ -0,0 +1,90 @@
1
+ # DelacourProvider
2
+
3
+ Every provider an app needs at its root, in one component. Mount it once, around
4
+ everything — a root layout, an `App.tsx`.
5
+
6
+ `import { DelacourProvider } from "@delacour/react-native-ui/provider";`
7
+
8
+ ## Files
9
+
10
+ | File | What it holds |
11
+ | --- | --- |
12
+ | `index.ts` | → `@delacour/react-native-ui/provider` |
13
+ | `provider.tsx` | `DelacourProvider` — the app's root layer stack; its `displayName` is `DelacourUI.Provider`, because prefix-plus-symbol would stutter and this matches its export subpath |
14
+
15
+ ## Design
16
+
17
+ - **Five layers, outermost first**: `GestureHandlerRootView` →
18
+ `SafeAreaProvider` → `KeyboardProvider` → `<KeyboardStateSync />` beside the
19
+ children → `BottomSheetModalProvider` around them. The order is not stylistic.
20
+ The gesture root has to be an ancestor native view of every handler a
21
+ `Pressable` creates, and its absence is *silent* — no error, no warning,
22
+ presses simply stop landing. `KeyboardStateSync` has to be a CHILD of
23
+ `KeyboardProvider`, because it calls `useKeyboardContext()`, and it stays a
24
+ SIBLING of the modal provider rather than moving inside it: the repair is
25
+ global and has to outlive any layer that can remount.
26
+ - **`initialMetrics` defaults to `initialWindowMetrics`, and that is
27
+ load-bearing.** `SafeAreaProvider` renders `null` — not unstyled children,
28
+ *nothing* — until its native view reports the first `onInsetsChange`, so
29
+ without the seed every cold start shows a blank frame. The seed is a snapshot
30
+ taken at native module init, so it is stale when the app launches into a
31
+ rotated or split-screen window — stale for exactly one commit, because the
32
+ native measurement overwrites it. A blank frame on every launch is the worse
33
+ trade. `initialMetrics={null}` opts out: a default parameter only fires on
34
+ `undefined`, so `null` is a value rather than an absence, the same rule
35
+ `pressedScale` follows.
36
+ - **`style` reaches the gesture root and carries no default.**
37
+ `GestureHandlerRootView` applies its own `{ flex: 1 }` whenever `style` is
38
+ undefined, so merging one in here would both duplicate it and make the prop
39
+ behave differently than it does upstream. Pass a style and that `flex: 1` is
40
+ gone — include it yourself.
41
+ - **No per-layer escape hatches, and no layer-named props.** No
42
+ `gestureHandler={false}`, no `safeAreaProps`, no `keyboardProps`. A boolean
43
+ that turns off the gesture root has "nothing responds to a press" as its
44
+ failure mode, which is the least debuggable outcome in the package. And a prop
45
+ surface that names the layers changes shape every time a layer is added:
46
+ `children`, `style` and `initialMetrics` say nothing about what is inside, so a
47
+ future `BottomSheetModalProvider` or portal host is an edit to one file rather
48
+ than a breaking change. An app that genuinely needs a different stack composes
49
+ the providers by hand — they are all public from their own packages, and this
50
+ is a convenience, not a gate.
51
+ - **A new layer goes innermost.** Anything that draws above the app — a
52
+ bottom-sheet modal provider, a portal host, a toast host — has to sit inside
53
+ every provider it reads, so it wraps `{children}` and nothing else moves. A
54
+ layer that brings a new native peer is a peer-dependency decision first.
55
+ `BottomSheetModalProvider` is the case this rule was written for and now the
56
+ case it governs: it reads the gesture root for its pan, the safe area for its
57
+ insets and the keyboard values a sheet's footer rides, so it is last.
58
+ - **`@gorhom/bottom-sheet` is a required peer because of this component**, on
59
+ exactly the argument `react-native-keyboard-controller` already carries. It was
60
+ optional while nothing imported it. The recommended root now does, so every app
61
+ resolves it, and a flag saying otherwise would only suppress the install
62
+ warning that explains the Metro resolution error coming out of the app's root
63
+ layout. It is pure JavaScript over Reanimated and Gesture Handler — both
64
+ required peers already — so this costs an install and no native build. Rule 3's
65
+ promise survives intact: `/button` still pulls nothing sheet-related.
66
+ - **Deliberately not idempotent.** It does not detect an enclosing copy of
67
+ itself. Nesting `GestureHandlerRootView` costs a `View`; nesting
68
+ `SafeAreaProvider` seeds from the parent's insets and costs a native view;
69
+ nesting `KeyboardProvider` is the one that actually breaks — two pairs of
70
+ shared values, two sets of native observers, and the outer `KeyboardStateSync`
71
+ repairing values nobody reads. That is also the only layer that cannot be
72
+ detected: `useKeyboardContext()` returns a module-private default object
73
+ outside a provider, that object is not exported, and the hook `console.warn`s
74
+ whenever it hands one back, so a detection read would print a warning in every
75
+ correctly-mounted app. A guard covering the two harmless layers and missing the
76
+ harmful one is worse than none — it teaches callers that nesting is fine.
77
+ - **`react-native-keyboard-controller` is a required peer because of this
78
+ component.** It was optional while [`Screen`](../screen/AGENTS.md) was the only importer: an app that
79
+ never imported `Screen` never resolved it. The recommended root now imports it,
80
+ so every app resolves it, and the flag had stopped describing reality — all it
81
+ still did was suppress the install warning that would have explained the Metro
82
+ resolution error coming out of the app's root layout. Rule 3's promise is
83
+ per-subpath and survives intact: `/button` still pulls nothing
84
+ keyboard-related.
85
+ - **Nothing here for `bun test`, and no `provider.variants.ts` to give it
86
+ some.** The component is four nested elements and one default parameter;
87
+ extracting a `resolveInitialMetrics()` would be a unit test of `??`. The rule
88
+ that pure decisions live in a `*.variants.ts` has no decision here to
89
+ relocate.
90
+ - **No gallery route.** See [Adding a component](../../../AGENTS.md#adding-a-component).
@@ -0,0 +1 @@
1
+ export { DelacourProvider, type DelacourProviderProps } from "./provider";
@@ -0,0 +1,105 @@
1
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import type { StyleProp, ViewStyle } from "react-native";
4
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
5
+ import { KeyboardProvider } from "react-native-keyboard-controller";
6
+ import { initialWindowMetrics, type Metrics, SafeAreaProvider } from "react-native-safe-area-context";
7
+ import { KeyboardStateSync } from "../../hooks/use-keyboard-state-sync";
8
+
9
+ export type DelacourProviderProps = {
10
+ children: ReactNode;
11
+ /**
12
+ * Safe-area insets and frame to render the first frame against, before the
13
+ * native provider has measured anything.
14
+ *
15
+ * Defaults to `initialWindowMetrics`, the snapshot the native module captured
16
+ * at launch, because `SafeAreaProvider` renders NOTHING — not unstyled
17
+ * children, `null` — until its first `onInsetsChange` lands. Without a seed
18
+ * every cold start shows a blank frame.
19
+ *
20
+ * Pass `null` to opt out. A default parameter only fires on `undefined`, so
21
+ * `null` is a value here rather than an absence.
22
+ */
23
+ initialMetrics?: Metrics | null;
24
+ /**
25
+ * Style for the outermost `GestureHandlerRootView`.
26
+ *
27
+ * Forwarded untouched, with no default merged in: the gesture root applies
28
+ * its own `{ flex: 1 }` whenever `style` is undefined. Pass one and that
29
+ * default is gone, so include `flex: 1` unless the root genuinely should not
30
+ * fill the window.
31
+ */
32
+ style?: StyleProp<ViewStyle>;
33
+ };
34
+
35
+ /**
36
+ * Every provider this library needs at an app's root, in one component.
37
+ *
38
+ * Mount it ONCE, around everything — a root layout, an `App.tsx`. It is not
39
+ * idempotent and does not detect an enclosing copy of itself; see AGENTS.md.
40
+ *
41
+ * Five layers, outermost first, and the order is not stylistic:
42
+ *
43
+ * 1. `GestureHandlerRootView` — an ancestor native view every gesture handler
44
+ * `Pressable` creates has to attach to. Its absence is silent: no error, no
45
+ * warning, presses simply stop landing.
46
+ * 2. `SafeAreaProvider` — the insets `Screen`'s navbar, footer and scroll
47
+ * reserves are all computed from, seeded so the first frame is not blank.
48
+ * 3. `KeyboardProvider` — the shared animation values `Screen.Footer` rides.
49
+ * 4. `KeyboardStateSync` — a child of the keyboard provider, because it calls
50
+ * `useKeyboardContext()`. It repairs the one pair of animation values that
51
+ * provider shares with the whole app: on iOS they are written only from the
52
+ * `will` events, so a keyboard that vanishes without one — an interactive
53
+ * dismiss interrupted by navigation, a stack pop, an app suspend — leaves
54
+ * every screen in the app believing it is still open.
55
+ * 5. `BottomSheetModalProvider` — the host every `BottomSheet` portals into. It
56
+ * goes innermost because it draws ABOVE the app and reads every layer over
57
+ * it: the gesture root for the pan, the safe area for its insets, and the
58
+ * keyboard values a sheet's footer rides. It wraps `{children}` and nothing
59
+ * else moves, which is what a new layer here always looks like.
60
+ *
61
+ * `KeyboardStateSync` stays a SIBLING of it rather than a child. The repair is
62
+ * global and has to run for the whole app's lifetime; inside a layer that can
63
+ * remount it would be torn down with it.
64
+ *
65
+ * There are no per-layer escape hatches and no layer-named props on purpose.
66
+ * An app that needs a different stack composes the providers by hand; they are
67
+ * all public from their own packages, and this is a convenience, not a gate.
68
+ *
69
+ * @example
70
+ * // expo-router root layout. The css import must stay the first statement.
71
+ * import "../styles/global.css";
72
+ * import { DelacourProvider } from "@delacour/react-native-ui/provider";
73
+ * import { Stack } from "expo-router";
74
+ *
75
+ * export default function RootLayout() {
76
+ * return (
77
+ * <DelacourProvider>
78
+ * <Stack screenOptions={{ headerShown: false }} />
79
+ * </DelacourProvider>
80
+ * );
81
+ * }
82
+ *
83
+ * @example
84
+ * // Measure the safe area from scratch, accepting the blank first frame — an
85
+ * // app that launches into a rotated or split-screen window and cannot
86
+ * // tolerate one stale frame.
87
+ * <DelacourProvider initialMetrics={null}>{children}</DelacourProvider>
88
+ */
89
+ export function DelacourProvider({
90
+ children,
91
+ initialMetrics = initialWindowMetrics,
92
+ style,
93
+ }: DelacourProviderProps): ReactElement {
94
+ return (
95
+ <GestureHandlerRootView style={style}>
96
+ <SafeAreaProvider initialMetrics={initialMetrics}>
97
+ <KeyboardProvider>
98
+ <KeyboardStateSync />
99
+ <BottomSheetModalProvider>{children}</BottomSheetModalProvider>
100
+ </KeyboardProvider>
101
+ </SafeAreaProvider>
102
+ </GestureHandlerRootView>
103
+ );
104
+ }
105
+ DelacourProvider.displayName = "DelacourUI.Provider";