jfs-components 0.0.86 → 0.0.99

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 (34) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commonjs/assets.d.js +1 -0
  3. package/lib/commonjs/components/Drawer/Drawer.js +146 -82
  4. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +118 -51
  5. package/lib/commonjs/components/Icon/Icon.js +112 -0
  6. package/lib/commonjs/components/Spinner/Spinner.js +5 -1
  7. package/lib/commonjs/components/index.js +7 -0
  8. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  9. package/lib/commonjs/icons/registry.js +1 -1
  10. package/lib/commonjs/skeleton/Skeleton.js +10 -2
  11. package/lib/module/assets.d.js +1 -0
  12. package/lib/module/components/Drawer/Drawer.js +148 -84
  13. package/lib/module/components/FullscreenModal/FullscreenModal.js +120 -53
  14. package/lib/module/components/Icon/Icon.js +106 -0
  15. package/lib/module/components/Spinner/Spinner.js +6 -2
  16. package/lib/module/components/index.js +1 -0
  17. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  18. package/lib/module/icons/registry.js +1 -1
  19. package/lib/module/skeleton/Skeleton.js +11 -3
  20. package/lib/typescript/src/components/Drawer/Drawer.d.ts +23 -4
  21. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +35 -21
  22. package/lib/typescript/src/components/Icon/Icon.d.ts +75 -0
  23. package/lib/typescript/src/components/index.d.ts +2 -1
  24. package/lib/typescript/src/icons/registry.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/assets.d.ts +24 -0
  27. package/src/components/Drawer/Drawer.tsx +94 -15
  28. package/src/components/FullscreenModal/FullscreenModal.tsx +146 -63
  29. package/src/components/Icon/Icon.tsx +167 -0
  30. package/src/components/Spinner/Spinner.tsx +2 -2
  31. package/src/components/index.ts +2 -1
  32. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  33. package/src/icons/registry.ts +1 -1
  34. package/src/skeleton/Skeleton.tsx +10 -3
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useId, useMemo, useState } from 'react';
4
- import { StyleSheet, View } from 'react-native';
4
+ import { View } from 'react-native';
5
5
  import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated';
6
6
  import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from 'react-native-svg';
7
7
  import { getVariableByName } from '../design-tokens/figma-variables-resolver';
@@ -16,12 +16,20 @@ const SOLID_OVERLAY_COLOR = 'rgba(255, 255, 255, 1)';
16
16
  // `pointerEvents: 'none'` lives on the style (not the deprecated prop) so it
17
17
  // works on both native and React Native Web without warnings.
18
18
  const absoluteFillStyle = {
19
- ...StyleSheet.absoluteFillObject,
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ right: 0,
23
+ bottom: 0,
20
24
  overflow: 'hidden',
21
25
  pointerEvents: 'none'
22
26
  };
23
27
  const solidOverlayStyle = {
24
- ...StyleSheet.absoluteFillObject,
28
+ position: 'absolute',
29
+ top: 0,
30
+ left: 0,
31
+ right: 0,
32
+ bottom: 0,
25
33
  backgroundColor: SOLID_OVERLAY_COLOR
26
34
  };
27
35
 
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- type DrawerProps = {
2
+ export type DrawerProps = {
3
3
  modes?: Record<string, any>;
4
4
  style?: import('react-native').StyleProp<import('react-native').ViewStyle>;
5
5
  title?: string;
@@ -11,7 +11,19 @@ type DrawerProps = {
11
11
  children?: React.ReactNode;
12
12
  collapsedHeight?: number;
13
13
  expandedRatio?: number;
14
+ /**
15
+ * Sets the drawer state when it first mounts (uncontrolled mode).
16
+ * Ignored once `state` is provided (controlled mode).
17
+ */
14
18
  initialState?: 'collapsed' | 'expanded';
19
+ /**
20
+ * Programmatically controls the drawer's collapsed/expanded state.
21
+ * When provided, the drawer becomes a controlled component: it will
22
+ * animate to match this value (reusing the same spring used by gestures)
23
+ * and gesture-driven changes are reported via `onStateChange` for the
24
+ * parent to apply back. Leave undefined for uncontrolled behaviour.
25
+ */
26
+ state?: 'collapsed' | 'expanded';
15
27
  contentStyle?: any;
16
28
  sheetStyle?: any;
17
29
  accessibilityLabel?: string;
@@ -31,9 +43,16 @@ type DrawerProps = {
31
43
  onStateChange?: (state: 'collapsed' | 'expanded') => void;
32
44
  };
33
45
  /**
34
- * Drawer component with nested scrolling support.
35
- * Uses react-native-gesture-handler and react-native-reanimated.
46
+ * Imperative handle exposed via `ref` for programmatic control of the drawer.
47
+ * Each method animates using the same spring as gesture-driven transitions.
36
48
  */
37
- declare function Drawer({ modes, style, title, header, children, collapsedHeight, expandedRatio, initialState, contentStyle, sheetStyle, accessibilityLabel, accessibilityHint, contentContainerStyle, showsVerticalScrollIndicator, bottomInset, onStateChange, }: DrawerProps): import("react/jsx-runtime").JSX.Element;
49
+ export type DrawerHandle = {
50
+ expand: () => void;
51
+ collapse: () => void;
52
+ toggle: () => void;
53
+ /** Current settled state of the drawer. */
54
+ getState: () => 'collapsed' | 'expanded';
55
+ };
56
+ declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<DrawerHandle>>;
38
57
  export default Drawer;
39
58
  //# sourceMappingURL=Drawer.d.ts.map
@@ -10,18 +10,21 @@ export type FullscreenModalProps = {
10
10
  /** Secondary line below the supporting paragraph (e.g. a price / timeline). */
11
11
  priceText?: string;
12
12
  /**
13
- * Background media rendered full-bleed behind the hero text. Bring any
14
- * renderer most commonly an `Image`, but a `LottiePlayer`, `Video`, or
15
- * `SvgXml` works too. It is laid out at the full modal width; size it with an
16
- * aspect ratio (e.g. `<Image ratio={3 / 4} />`) so its height follows the
17
- * width naturally. The media scrolls together with the rest of the content
18
- * (no parallax). `modes` are cascaded into it.
13
+ * Full-bleed background media for the whole modal. It is pinned to the top
14
+ * and laid out at the full modal width; size it with an aspect ratio
15
+ * (e.g. `<Image ratio={1080 / 4140} />`) so its height follows the width
16
+ * naturally. It renders as a single continuous background BEHIND both the
17
+ * hero text and the body content there is no separate body box stacked on
18
+ * top of it. Bring any renderer most commonly an `Image`, but a
19
+ * `LottiePlayer`, `Video`, or `SvgXml` works too. It never intercepts
20
+ * touches and the foreground content scrolls over it (no parallax).
21
+ * `modes` are cascaded into it.
19
22
  */
20
23
  heroMedia?: React.ReactNode;
21
24
  /**
22
- * Fallback height for the hero text region when no `heroMedia` is provided.
23
- * When `heroMedia` is present, the hero height is driven entirely by the
24
- * media's own aspect ratio and this value is ignored. Defaults to 420.
25
+ * Height reserved for the hero text region (eyebrow / headline / supporting
26
+ * / price), whose content is anchored to the bottom. Applies whether or not
27
+ * `heroMedia` is present. Defaults to 420.
25
28
  */
26
29
  heroHeight?: number;
27
30
  /** Whether to render the floating close button (top-right). Defaults to true. */
@@ -41,15 +44,17 @@ export type FullscreenModalProps = {
41
44
  onPrimaryAction?: () => void;
42
45
  /** Disclaimer text shown below the default primary action button. */
43
46
  disclaimer?: string;
44
- /** Solid backdrop color for the scrollable body. Defaults to a near-black. */
45
- backgroundColor?: string;
46
47
  /** Body content (typically `Section`s). `modes` are cascaded automatically. */
47
48
  children?: React.ReactNode;
48
- /** Mode configuration. `context5` is always forced to `'Fullscreen Modal'`. */
49
+ /**
50
+ * Mode configuration. `Page type` defaults to `'JioPlus'` (overridable here),
51
+ * and `context5` is always forced to `'Fullscreen Modal'` (non-overridable).
52
+ * The resolved modes cascade to the body, hero media, and the `ActionFooter`.
53
+ */
49
54
  modes?: Record<string, any>;
50
55
  /** Style overrides for the outer container. */
51
56
  style?: StyleProp<ViewStyle>;
52
- /** Style overrides for the scroll body wrapper (the dark content area). */
57
+ /** Style overrides for the transparent body wrapper. */
53
58
  contentContainerStyle?: StyleProp<ViewStyle>;
54
59
  testID?: string;
55
60
  };
@@ -64,12 +69,21 @@ export type FullscreenModalProps = {
64
69
  * That mode is cascaded into `children`, the footer, and the hero text via
65
70
  * `cloneChildrenWithModes` / the merged `modes` object.
66
71
  *
67
- * ### Hero
68
- * The `heroMedia` is rendered full modal width inside the scroll body and
69
- * takes its height from its own aspect ratio. The hero text (eyebrow /
70
- * headline / supporting / price) is overlaid on top, anchored to the bottom.
71
- * The whole hero scrolls together with the rest of the content there is no
72
- * parallax effect.
72
+ * ### Background media
73
+ * The `heroMedia` is a single full-bleed background pinned to the top of the
74
+ * modal at the full width and its own natural aspect ratio. It lives at the
75
+ * ROOT behind both the scrolling content and the (transparent) footer so
76
+ * it fills the whole surface and is NEVER clipped to the content height. It
77
+ * also contributes ZERO scroll height: the scroll extent is driven purely by
78
+ * the in-flow foreground (hero text + `children`), so the number of body
79
+ * elements dictates how far the surface scrolls. It still scrolls in lockstep
80
+ * WITH the content (the background is translated by the scroll offset), so the
81
+ * content reads as sitting ON one continuous image that moves with it — there
82
+ * is no parallax and no separate solid body box.
83
+ *
84
+ * Pass a background sized to the full width at its natural ratio
85
+ * (e.g. `<Image imageSource={bg} ratio={1080 / 4140} />`). Use an asset at
86
+ * least as tall as the surface so it covers the full modal.
73
87
  *
74
88
  * @component
75
89
  * @example
@@ -79,7 +93,7 @@ export type FullscreenModalProps = {
79
93
  * headline="Get more from your money."
80
94
  * supportingText="JioFinance+ is your upgraded financial experience…"
81
95
  * priceText="₹999/year · ₹0 until 2027"
82
- * heroMedia={<Image imageSource={hero} ratio={3 / 4} />}
96
+ * heroMedia={<Image imageSource={hero} ratio={1080 / 4140} />}
83
97
  * primaryActionLabel="Upgrade for free"
84
98
  * disclaimer="By upgrading, we'll check your eligibility with Experian."
85
99
  * onPrimaryAction={() => upgrade()}
@@ -90,6 +104,6 @@ export type FullscreenModalProps = {
90
104
  * </FullscreenModal>
91
105
  * ```
92
106
  */
93
- declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, backgroundColor, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
107
+ declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
94
108
  export default FullscreenModal;
95
109
  //# sourceMappingURL=FullscreenModal.d.ts.map
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import { type AccessibilityProps, type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { UnifiedSource } from '../../utils/MediaSource';
4
+ type IconProps = AccessibilityProps & {
5
+ /**
6
+ * Built-in icon name from the registry, in the `ic_something` format
7
+ * (e.g. `'ic_card'`, `'ic_scan_qr_code'`). When omitted and no `source` or
8
+ * `children` slot is supplied, defaults to `'ic_card'` to match the Figma
9
+ * design's default glyph.
10
+ */
11
+ iconName?: string;
12
+ /**
13
+ * Unified fallback source rendered when `iconName` is missing or not in the
14
+ * registry. Accepts a remote URI, an inline SVG XML string, a `require()`
15
+ * asset, an SVG React component, or an already-rendered element. The media
16
+ * is tinted with the mode-resolved icon color so it follows design tokens
17
+ * just like a built-in icon. See {@link UnifiedSource}.
18
+ */
19
+ source?: UnifiedSource;
20
+ /**
21
+ * Icon slot. Render any node here (another `Icon`, a custom SVG component,
22
+ * etc.) and it takes precedence over `iconName`/`source`. `modes` cascade
23
+ * into the slotted children automatically.
24
+ */
25
+ children?: React.ReactNode;
26
+ /**
27
+ * Override the mode-resolved icon color. When omitted the value comes from
28
+ * the `icon/color` design token.
29
+ */
30
+ color?: string;
31
+ /**
32
+ * Override the mode-resolved icon size (in px). When omitted the value comes
33
+ * from the `icon/size` design token.
34
+ */
35
+ size?: number;
36
+ modes?: Record<string, any>;
37
+ style?: StyleProp<ViewStyle>;
38
+ };
39
+ /**
40
+ * Icon component — a design-token-driven wrapper around a single glyph.
41
+ *
42
+ * It mirrors the Figma "Icon" component: a padded, centered container whose
43
+ * color and size are resolved from the `icon/*` design tokens via `modes`.
44
+ * The glyph itself can be supplied three ways, in order of precedence:
45
+ *
46
+ * 1. `children` — a real slot for any node (custom SVG component, nested
47
+ * `Icon`, etc.). `modes` cascade into the slot automatically.
48
+ * 2. `iconName` — a registry icon in the `ic_something` format.
49
+ * 3. `source` — a {@link UnifiedSource} fallback (remote URI, inline SVG XML,
50
+ * `require()` asset, SVG component, or React element), tinted with the
51
+ * mode-resolved icon color.
52
+ *
53
+ * `color` and `size` props let consumers override the token values per
54
+ * instance without touching `modes`.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * // Built-in registry icon (default path).
59
+ * <Icon iconName="ic_card" modes={{ 'Color Mode': 'Light' }} />
60
+ *
61
+ * // Per-instance overrides.
62
+ * <Icon iconName="ic_ccv" color="#5c00b5" size={24} />
63
+ *
64
+ * // Fallback to an external source when the name isn't in the registry.
65
+ * <Icon source="https://cdn.example.com/glyph.svg" />
66
+ *
67
+ * // Slot: render any node as the icon.
68
+ * <Icon><BrandLogo /></Icon>
69
+ * ```
70
+ */
71
+ declare function Icon({ iconName, source, children, color, size, modes: propModes, style: styleProp, ...rest }: IconProps): import("react/jsx-runtime").JSX.Element;
72
+ declare const _default: React.MemoExoticComponent<typeof Icon>;
73
+ export default _default;
74
+ export type { IconProps };
75
+ //# sourceMappingURL=Icon.d.ts.map
@@ -22,7 +22,7 @@ export { default as CardFinancialCondition, type CardFinancialConditionProps } f
22
22
  export { default as CardInsight, type CardInsightProps } from './CardInsight/CardInsight';
23
23
  export { default as Disclaimer } from './Disclaimer/Disclaimer';
24
24
  export { default as Divider, type DividerProps, type DividerDirection } from './Divider/Divider';
25
- export { default as Drawer } from './Drawer/Drawer';
25
+ export { default as Drawer, type DrawerProps, type DrawerHandle } from './Drawer/Drawer';
26
26
  export { default as Dropdown, DropdownItem, type DropdownProps, type DropdownItemProps } from './Dropdown/Dropdown';
27
27
  export { default as DropdownInput, type DropdownInputProps, type DropdownInputOption, type DropdownInputOptionValue } from './DropdownInput/DropdownInput';
28
28
  export { default as SuggestiveSearch, type SuggestiveSearchProps, type SuggestiveSearchOption, type SuggestiveSearchOptionValue, type SuggestiveSearchItem } from './SuggestiveSearch/SuggestiveSearch';
@@ -43,6 +43,7 @@ export { default as MonthlyStatusGrid, CalendarGlyph, type MonthlyStatusGridProp
43
43
  export { default as Gauge, type GaugeProps } from './Gauge/Gauge';
44
44
  export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/HoldingsCard';
45
45
  export { default as HStack, type HStackProps } from './HStack/HStack';
46
+ export { default as Icon, type IconProps } from './Icon/Icon';
46
47
  export { default as IconButton } from './IconButton/IconButton';
47
48
  export { default as IconCapsule } from './IconCapsule/IconCapsule';
48
49
  export { default as Image, type ImageProps } from './Image/Image';
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-06-03T15:59:02.370Z
7
+ * Generated: 2026-06-08T14:11:35.036Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.86",
3
+ "version": "0.0.99",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -0,0 +1,24 @@
1
+ declare module '*.png' {
2
+ const value: string
3
+ export default value
4
+ }
5
+
6
+ declare module '*.jpg' {
7
+ const value: string
8
+ export default value
9
+ }
10
+
11
+ declare module '*.jpeg' {
12
+ const value: string
13
+ export default value
14
+ }
15
+
16
+ declare module '*.gif' {
17
+ const value: string
18
+ export default value
19
+ }
20
+
21
+ declare module '*.webp' {
22
+ const value: string
23
+ export default value
24
+ }
@@ -1,9 +1,8 @@
1
- import React, { useCallback, useEffect, useState, useRef } from 'react'
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
2
2
  import { Platform, StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'
3
3
  import {
4
4
  Gesture,
5
5
  GestureDetector,
6
- GestureHandlerRootView,
7
6
  ScrollView,
8
7
  } from 'react-native-gesture-handler'
9
8
  import Animated, {
@@ -53,7 +52,7 @@ function rubberBand(value: number, min: number, max: number, friction: number =
53
52
  return value
54
53
  }
55
54
 
56
- type DrawerProps = {
55
+ export type DrawerProps = {
57
56
 
58
57
  modes?: Record<string, any>
59
58
  style?: import('react-native').StyleProp<import('react-native').ViewStyle>
@@ -66,7 +65,19 @@ type DrawerProps = {
66
65
  children?: React.ReactNode
67
66
  collapsedHeight?: number
68
67
  expandedRatio?: number
68
+ /**
69
+ * Sets the drawer state when it first mounts (uncontrolled mode).
70
+ * Ignored once `state` is provided (controlled mode).
71
+ */
69
72
  initialState?: 'collapsed' | 'expanded'
73
+ /**
74
+ * Programmatically controls the drawer's collapsed/expanded state.
75
+ * When provided, the drawer becomes a controlled component: it will
76
+ * animate to match this value (reusing the same spring used by gestures)
77
+ * and gesture-driven changes are reported via `onStateChange` for the
78
+ * parent to apply back. Leave undefined for uncontrolled behaviour.
79
+ */
80
+ state?: 'collapsed' | 'expanded'
70
81
  contentStyle?: any
71
82
  sheetStyle?: any
72
83
  accessibilityLabel?: string
@@ -86,11 +97,23 @@ type DrawerProps = {
86
97
  onStateChange?: (state: 'collapsed' | 'expanded') => void
87
98
  }
88
99
 
100
+ /**
101
+ * Imperative handle exposed via `ref` for programmatic control of the drawer.
102
+ * Each method animates using the same spring as gesture-driven transitions.
103
+ */
104
+ export type DrawerHandle = {
105
+ expand: () => void
106
+ collapse: () => void
107
+ toggle: () => void
108
+ /** Current settled state of the drawer. */
109
+ getState: () => 'collapsed' | 'expanded'
110
+ }
111
+
89
112
  /**
90
113
  * Drawer component with nested scrolling support.
91
114
  * Uses react-native-gesture-handler and react-native-reanimated.
92
115
  */
93
- function Drawer({
116
+ function DrawerInner({
94
117
 
95
118
  modes = EMPTY_MODES,
96
119
  style,
@@ -100,6 +123,7 @@ function Drawer({
100
123
  collapsedHeight = 200,
101
124
  expandedRatio = 0.90,
102
125
  initialState = 'collapsed',
126
+ state,
103
127
  contentStyle,
104
128
  sheetStyle,
105
129
  accessibilityLabel,
@@ -108,7 +132,7 @@ function Drawer({
108
132
  showsVerticalScrollIndicator = false,
109
133
  bottomInset = 80,
110
134
  onStateChange,
111
- }: DrawerProps) {
135
+ }: DrawerProps, ref: React.Ref<DrawerHandle>) {
112
136
  const { height: screenHeight } = useWindowDimensions()
113
137
 
114
138
  // Calculate snap points
@@ -168,15 +192,58 @@ function Drawer({
168
192
  translateY.value = withSpring(destination, SPRING_CONFIG)
169
193
  }, [translateY])
170
194
 
171
- // Update JS state for accessibility/logic if needed
195
+ // Update the JS-side mode. Pure: only schedules the state update. Side
196
+ // effects (notifying the parent via onStateChange) MUST NOT live inside the
197
+ // setState updater — doing so calls the parent's setState during React's
198
+ // render phase, which throws "Cannot update a component while rendering a
199
+ // different component" and causes an infinite update loop. We report changes
200
+ // from a dedicated effect below instead.
172
201
  const updateMode = useCallback((newMode: 'collapsed' | 'expanded') => {
173
- setMode((prev) => {
174
- if (prev !== newMode) {
175
- onStateChange?.(newMode)
176
- }
177
- return newMode
178
- })
179
- }, [onStateChange])
202
+ setMode(newMode)
203
+ }, [])
204
+
205
+ // Notify the parent exactly once per settled mode change, after commit.
206
+ const reportedModeRef = useRef(mode)
207
+ useEffect(() => {
208
+ if (reportedModeRef.current !== mode) {
209
+ reportedModeRef.current = mode
210
+ onStateChange?.(mode)
211
+ }
212
+ }, [mode, onStateChange])
213
+
214
+ // Programmatic transition (JS thread). Reuses the same spring as gestures:
215
+ // animates `translateY`, keeps the scroll-gate (`isFullyExpanded`) in sync,
216
+ // and updates `mode` (which notifies via the onStateChange effect above).
217
+ const applyMode = useCallback((newMode: 'collapsed' | 'expanded') => {
218
+ const target = newMode === 'expanded' ? minTranslateY : maxTranslateY
219
+ translateY.value = withSpring(target, SPRING_CONFIG)
220
+ isFullyExpanded.value = newMode === 'expanded'
221
+ setMode(newMode)
222
+ }, [minTranslateY, maxTranslateY, translateY, isFullyExpanded])
223
+
224
+ // Controlled mode: react ONLY to genuine changes of the `state` prop, tracked
225
+ // against its previous value. We must NOT reconcile `state` against the
226
+ // internal `mode` on every render: a gesture updates `mode` optimistically
227
+ // one render before the parent echoes it back through `onStateChange` into
228
+ // `state`, so a `state !== mode` check would "correct" the gesture and fight
229
+ // the echo, ping-ponging forever (Maximum update depth exceeded).
230
+ // `applyMode` is idempotent, so re-applying the already-current mode is a
231
+ // no-op when the parent simply mirrors our own change back.
232
+ const prevStateProp = useRef(state)
233
+ useEffect(() => {
234
+ if (state === undefined) return
235
+ if (state === prevStateProp.current) return
236
+ prevStateProp.current = state
237
+ applyMode(state)
238
+ }, [state, applyMode])
239
+
240
+ // Imperative API for parents holding a ref.
241
+ useImperativeHandle(ref, () => ({
242
+ expand: () => applyMode('expanded'),
243
+ collapse: () => applyMode('collapsed'),
244
+ toggle: () => applyMode(mode === 'expanded' ? 'collapsed' : 'expanded'),
245
+ getState: () => mode,
246
+ }), [applyMode, mode])
180
247
 
181
248
  // Gesture policy:
182
249
  // • activeOffsetY: require a clear *vertical* drag (10px) before this
@@ -362,7 +429,16 @@ function Drawer({
362
429
  const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer'
363
430
 
364
431
  return (
365
- <GestureHandlerRootView style={[styles.host, style]} pointerEvents="box-none">
432
+ // IMPORTANT: the host is a plain box-none View, NOT a GestureHandlerRootView.
433
+ // On Android, GestureHandlerRootView renders a *native* root view that
434
+ // intercepts every touch within its bounds (to feed the gesture system) and
435
+ // does NOT honor pointerEvents="box-none". Because this host fills the whole
436
+ // screen as an overlay, using GestureHandlerRootView here swallowed all
437
+ // touches to content rendered behind the drawer (buttons, page content).
438
+ // Per the standard react-native-gesture-handler architecture, a single
439
+ // GestureHandlerRootView must wrap the app root; this overlay only needs to
440
+ // let touches fall through where the sheet isn't.
441
+ <View style={[styles.host, style]} pointerEvents="box-none">
366
442
  <GestureDetector gesture={gesture}>
367
443
  <Animated.View
368
444
  style={[
@@ -457,7 +533,7 @@ function Drawer({
457
533
  </View>
458
534
  </Animated.View>
459
535
  </GestureDetector>
460
- </GestureHandlerRootView>
536
+ </View>
461
537
  )
462
538
  }
463
539
 
@@ -492,4 +568,7 @@ const styles = StyleSheet.create({
492
568
  },
493
569
  })
494
570
 
571
+ const Drawer = forwardRef<DrawerHandle, DrawerProps>(DrawerInner)
572
+ Drawer.displayName = 'Drawer'
573
+
495
574
  export default Drawer