@oxyhq/bloom 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +29 -12
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.js +8 -15
  4. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  5. package/lib/commonjs/dialog/Dialog.web.js +14 -15
  6. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  7. package/lib/commonjs/dialog/DialogBottomSheet.js +0 -2
  8. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  9. package/lib/commonjs/dialog/placement.js +1 -4
  10. package/lib/commonjs/dialog/placement.js.map +1 -1
  11. package/lib/commonjs/overlay/index.js +42 -21
  12. package/lib/commonjs/overlay/index.js.map +1 -1
  13. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +2 -5
  14. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  15. package/lib/module/bottom-sheet/BottomSheetBase.js +31 -14
  16. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  17. package/lib/module/dialog/Dialog.js +10 -17
  18. package/lib/module/dialog/Dialog.js.map +1 -1
  19. package/lib/module/dialog/Dialog.web.js +15 -16
  20. package/lib/module/dialog/Dialog.web.js.map +1 -1
  21. package/lib/module/dialog/DialogBottomSheet.js +1 -3
  22. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  23. package/lib/module/dialog/placement.js +0 -3
  24. package/lib/module/dialog/placement.js.map +1 -1
  25. package/lib/module/overlay/index.js +42 -23
  26. package/lib/module/overlay/index.js.map +1 -1
  27. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +2 -5
  28. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  29. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/dialog/placement.d.ts +0 -2
  34. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/overlay/index.d.ts +30 -5
  36. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  38. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  39. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  40. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  41. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  42. package/lib/typescript/module/dialog/placement.d.ts +0 -2
  43. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  44. package/lib/typescript/module/overlay/index.d.ts +30 -5
  45. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  46. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/src/__tests__/animated-style-deps.test.ts +1 -1
  49. package/src/__tests__/overlay-pointer-events.test.tsx +17 -16
  50. package/src/bottom-sheet/BottomSheetBase.tsx +30 -13
  51. package/src/dialog/Dialog.tsx +9 -17
  52. package/src/dialog/Dialog.web.tsx +16 -25
  53. package/src/dialog/DialogBottomSheet.tsx +0 -2
  54. package/src/dialog/placement.ts +0 -2
  55. package/src/overlay/index.tsx +68 -32
  56. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +6 -5
@@ -20,12 +20,13 @@ import Animated, {
20
20
  type SharedValue,
21
21
  useAnimatedScrollHandler,
22
22
  useAnimatedStyle,
23
+ useDerivedValue,
23
24
  useSharedValue,
24
25
  withSpring,
25
26
  withTiming,
26
27
  } from 'react-native-reanimated';
27
28
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
- import { Backdrop } from '../overlay';
29
+ import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
29
30
  import { useTheme } from '../theme/use-theme';
30
31
 
31
32
  /** Hook that returns current screen dimensions and updates on rotation/resize. */
@@ -199,8 +200,6 @@ export interface BottomSheetBaseProps extends BottomSheetProps {
199
200
  Shell: React.ComponentType<BottomSheetShellProps>;
200
201
  }
201
202
 
202
- const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
203
-
204
203
  export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: React.ForwardedRef<BottomSheetRef>) => {
205
204
  const {
206
205
  Shell,
@@ -215,7 +214,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
215
214
  onDismissAttempt,
216
215
  detached = false,
217
216
  showHandle = true,
218
- backdropOpacity = 0.5,
217
+ backdropOpacity = BACKDROP_DIM_OPACITY,
219
218
  scrollable = true,
220
219
  manualActivation = false,
221
220
  dynamicBackdrop = false,
@@ -642,7 +641,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
642
641
  // dim opacity applies smoothly across the animation. When
643
642
  // `dynamicBackdrop` is enabled, the dim also fades proportionally with
644
643
  // drag distance (iOS Photos style).
645
- const backdropStyle = useAnimatedStyle(() => {
644
+ // A VALUE, not a style: `Backdrop` applies it to its blur and dim layers
645
+ // itself. Handing it over as an animated style would put the opacity on the
646
+ // layers' shared ancestor, which composites the group in isolation and
647
+ // leaves `backdrop-filter` with nothing behind it to blur.
648
+ const backdropProgress = useDerivedValue(() => {
646
649
  const dragFactor = dynamicBackdrop
647
650
  ? interpolate(
648
651
  translateY.value,
@@ -651,10 +654,16 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
651
654
  'clamp',
652
655
  )
653
656
  : 1;
654
- return {
655
- opacity: opacity.value * backdropOpacity * dragFactor,
656
- };
657
- }, [backdropOpacity, dynamicBackdrop, opacity, translateY, screenHeightSV]);
657
+ return opacity.value * dragFactor;
658
+ }, [dynamicBackdrop, opacity, translateY, screenHeightSV]);
659
+
660
+ // Only the consumer-supplied `backdropComponent` needs the progress as a
661
+ // style — it owns its own visuals, blur included, so the ancestor-opacity
662
+ // hazard is its call to make.
663
+ const customBackdropStyle = useAnimatedStyle(
664
+ () => ({ opacity: backdropProgress.value }),
665
+ [backdropProgress],
666
+ );
658
667
 
659
668
  const sheetStyle = useAnimatedStyle(() => {
660
669
  const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
@@ -787,7 +796,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
787
796
  <Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
788
797
  <View style={StyleSheet.absoluteFill}>
789
798
  {backdropComponent ? (
790
- <Animated.View style={[StyleSheet.absoluteFill, backdropStyle]}>
799
+ <Animated.View style={[StyleSheet.absoluteFill, customBackdropStyle]}>
791
800
  {backdropComponent({ onPress: handleBackdropPress })}
792
801
  </Animated.View>
793
802
  ) : (
@@ -795,10 +804,18 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
795
804
  // `backdropStyle` drives the fade; the dim level rides on
796
805
  // `backdropOpacity`, so the shared component's own dim is
797
806
  // switched off here to keep a single source of dimming.
798
- <AnimatedBackdrop
807
+ <Backdrop
799
808
  onPress={handleBackdropPress}
800
- dimOpacity={1}
801
- style={[styles.backdrop, backdropStyle]}
809
+ // `progress` is the FADE (0 → 1, folding the drag);
810
+ // how dark the backdrop gets is `backdropOpacity`,
811
+ // which defaults to the library-wide dim so a sheet
812
+ // matches every other surface. It goes through
813
+ // `progress` rather than an animated style because an
814
+ // opacity on the root would sit above the blur layer
815
+ // and neutralise it.
816
+ dimOpacity={backdropOpacity}
817
+ progress={backdropProgress}
818
+ style={styles.backdrop}
802
819
  />
803
820
  )}
804
821
 
@@ -26,6 +26,7 @@ import Animated, {
26
26
  } from 'react-native-reanimated';
27
27
 
28
28
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
29
+ import { Backdrop } from '../overlay';
29
30
  import { Z_INDEX, Z_INDEX_LAYER_STEP } from '../styles/z-index';
30
31
  import { useTheme } from '../theme/use-theme';
31
32
  import { Context, useDialogControl } from './context';
@@ -46,7 +47,6 @@ import {
46
47
  DEFAULT_SIDE_WIDTH,
47
48
  DIALOG_SHEET_BACKDROP_TESTID,
48
49
  PANEL_RADIUS,
49
- SHEET_BACKDROP_OPACITY,
50
50
  SIDE_SHEET_MIN_GUTTER,
51
51
  useResolvedPlacement,
52
52
  } from './placement';
@@ -486,10 +486,6 @@ function SideSheet({
486
486
  if (dismissOnBackdrop) onDismiss();
487
487
  }, [dismissOnBackdrop, onDismiss]);
488
488
 
489
- const backdropAnimatedStyle = useAnimatedStyle(() => ({
490
- opacity: progress.value * SHEET_BACKDROP_OPACITY,
491
- }), []);
492
-
493
489
  const hiddenSign = side === 'left' ? -1 : 1;
494
490
  const panelAnimatedStyle = useAnimatedStyle(() => {
495
491
  const hidden = 1 - progress.value;
@@ -533,18 +529,14 @@ function SideSheet({
533
529
  ]}
534
530
  {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
535
531
  >
536
- <Animated.View
537
- style={[sideStyles.backdrop, backdropAnimatedStyle, { pointerEvents: 'auto' }]}
538
- >
539
- <Pressable
540
- testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
541
- accessibilityRole="button"
542
- accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
543
- onPress={handleBackdropPress}
544
- disabled={!dismissOnBackdrop}
545
- style={StyleSheet.absoluteFill}
546
- />
547
- </Animated.View>
532
+ <Backdrop
533
+ testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
534
+ accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
535
+ onPress={handleBackdropPress}
536
+ disabled={!dismissOnBackdrop}
537
+ progress={progress}
538
+ style={sideStyles.backdrop}
539
+ />
548
540
 
549
541
  <Animated.View
550
542
  accessibilityViewIsModal
@@ -47,7 +47,6 @@ import {
47
47
  DIALOG_SHEET_BACKDROP_TESTID,
48
48
  EASE_OUT,
49
49
  PANEL_RADIUS,
50
- SHEET_BACKDROP_OPACITY,
51
50
  SIDE_SHEET_MIN_GUTTER,
52
51
  useResolvedPlacement,
53
52
  } from './placement';
@@ -60,11 +59,6 @@ import type {
60
59
 
61
60
  const FADE_OUT_DURATION = CENTER_FADE_OUT_DURATION;
62
61
 
63
- /**
64
- * The centered card's scrim reads darker than a sheet's because the card floats
65
- * in the middle of it; the shared blur underneath is identical.
66
- */
67
- const DIALOG_BACKDROP_DIM_OPACITY = 0.6;
68
62
 
69
63
  /**
70
64
  * The four CSS `animation` shorthands driving the centered card and its backdrop.
@@ -331,17 +325,17 @@ function CenterOrSideDialog({
331
325
  onPress={() => close()}
332
326
  disabled={!dismissOnBackdrop}
333
327
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
334
- dimOpacity={DIALOG_BACKDROP_DIM_OPACITY}
335
- style={[
336
- {
337
- position: WEB_POSITION_FIXED,
338
- zIndex: dialogZIndex.backdrop,
339
- alignItems: 'center',
340
- justifyContent: 'center',
341
- paddingHorizontal: 20,
342
- },
343
- isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
344
- ]}
328
+ // The fade rides on the LAYERS, never on the press target: an
329
+ // opacity animation on the blur's ancestor composites the group in
330
+ // isolation and leaves `backdrop-filter` nothing to sample.
331
+ layerStyle={isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN}
332
+ style={{
333
+ position: WEB_POSITION_FIXED,
334
+ zIndex: dialogZIndex.backdrop,
335
+ alignItems: 'center',
336
+ justifyContent: 'center',
337
+ paddingHorizontal: 20,
338
+ }}
345
339
  >
346
340
  <DialogPanel
347
341
  testID={testID}
@@ -702,14 +696,11 @@ function SheetSurface({
702
696
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
703
697
  onPress={handleBackdropPress}
704
698
  disabled={!dismissOnBackdrop}
705
- // The dim rides on the element's own animated opacity here, so the
706
- // shared component's dim layer stays fully opaque under it.
707
- dimOpacity={1}
708
- style={[
709
- sheetStyles.backdrop,
710
- backdropTransition,
711
- { opacity: visible ? SHEET_BACKDROP_OPACITY : 0 },
712
- ]}
699
+ // Same reason as the centred dialog: the transition and the opacity it
700
+ // animates belong to the layers, not to the press target above them.
701
+ // No `dimOpacity` override — one darkness for every Bloom surface.
702
+ layerStyle={backdropTransition}
703
+ style={[sheetStyles.backdrop, { opacity: visible ? 1 : 0 }]}
713
704
  />
714
705
 
715
706
  <View
@@ -30,7 +30,6 @@ import {
30
30
  DEFAULT_DIALOG_CONTENT_PADDING,
31
31
  DEFAULT_MAX_HEIGHT_RATIO,
32
32
  PANEL_RADIUS,
33
- SHEET_BACKDROP_OPACITY,
34
33
  } from './placement';
35
34
  import type { DialogControlProps, DialogProps } from './types';
36
35
 
@@ -286,7 +285,6 @@ export function DialogBottomSheet({
286
285
  }
287
286
  // Stronger dim than a lone sheet so an underlying sheet's handle/content
288
287
  // doesn't bleed through when a Dialog is stacked over one.
289
- backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
290
288
  // Measures the sheet card — the size a morph starts from.
291
289
  onLayout={morphState.onPanelLayout}
292
290
  style={sheetStyle}
@@ -83,8 +83,6 @@ export const CENTER_FADE_OUT_DURATION = 150;
83
83
  /** CSS approximation of reanimated's `Easing.out(Easing.cubic)`. */
84
84
  export const EASE_OUT = 'cubic-bezier(0.33, 1, 0.68, 1)';
85
85
 
86
- /** Backdrop dim opacity once fully shown (matches `bg-black/40`). */
87
- export const SHEET_BACKDROP_OPACITY = 0.4;
88
86
 
89
87
  /** Stable testID for the dimmed backdrop of a side/bottom placement dialog. */
90
88
  export const DIALOG_SHEET_BACKDROP_TESTID = 'bloom-dialog-sheet-backdrop';
@@ -39,7 +39,7 @@
39
39
  * Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
40
40
  * dimming layer; do not re-implement either with raw `View`s.
41
41
  */
42
- import { forwardRef, memo, type ReactNode } from 'react';
42
+ import { memo, type ReactNode } from 'react';
43
43
  import { BlurView } from 'expo-blur';
44
44
  import {
45
45
  Platform,
@@ -49,16 +49,26 @@ import {
49
49
  type StyleProp,
50
50
  type ViewStyle,
51
51
  } from 'react-native';
52
+ import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
52
53
 
53
54
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
54
55
 
56
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
57
+
55
58
  /**
56
59
  * One blur radius for every Bloom overlay. Surfaces differ in what they show,
57
60
  * not in how the app behind them recedes.
58
61
  */
59
62
  export const BACKDROP_BLUR_INTENSITY = 80;
60
- /** Dim laid over the blur — the blur alone does not give enough contrast. */
61
- export const BACKDROP_DIM_OPACITY = 0.55;
63
+ /**
64
+ * Extra dim laid OVER the blur. The blur already paints its own tint wash
65
+ * (`rgba(25,25,25,~0.62)` at the intensity above), so this is what takes the
66
+ * total to roughly 0.72 — dark enough to read a panel against, still clearly
67
+ * see-through. Every surface uses this one value: with each of them picking its
68
+ * own, menus landed at ~0.83 and read as solid black next to the image viewer's
69
+ * ~0.72.
70
+ */
71
+ export const BACKDROP_DIM_OPACITY = 0.28;
62
72
 
63
73
  export interface OverlayRootProps {
64
74
  children?: ReactNode;
@@ -85,6 +95,12 @@ OverlayRoot.displayName = 'OverlayRoot';
85
95
  export interface BackdropProps {
86
96
  /** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
87
97
  onPress?: () => void;
98
+ /**
99
+ * Fade driver, 0 (hidden) → 1 (fully shown). The backdrop applies it to its
100
+ * OWN layers — see the note on `style` for why the caller must not animate
101
+ * opacity from the outside.
102
+ */
103
+ progress?: SharedValue<number>;
88
104
  /** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
89
105
  disabled?: boolean;
90
106
  /** Blur radius behind the dim. `0` renders the dim alone. */
@@ -95,8 +111,19 @@ export interface BackdropProps {
95
111
  dimColor?: string;
96
112
  /** Dim opacity, 0–1. */
97
113
  dimOpacity?: number;
98
- /** Extra style on the press target — animated opacity, insets, z-index. */
114
+ /**
115
+ * Geometry for the press target: insets, z-index, layout. NOT opacity —
116
+ * `backdrop-filter` samples nothing under an ancestor with `opacity < 1`
117
+ * (the group composites in isolation), so a fade applied here silently kills
118
+ * the blur. An `opacity` found in this style is redirected onto the layers;
119
+ * animate the fade through `progress` instead.
120
+ */
99
121
  style?: StyleProp<ViewStyle>;
122
+ /**
123
+ * Extra style for the blur + dim LAYERS, for fades a shared value can't
124
+ * express — the web dialog's CSS keyframes, for instance.
125
+ */
126
+ layerStyle?: StyleProp<ViewStyle>;
100
127
  /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
101
128
  children?: ReactNode;
102
129
  accessibilityLabel?: string;
@@ -108,31 +135,40 @@ export interface BackdropProps {
108
135
  * pointer events (that is its whole job), so anything that must stay pressable
109
136
  * goes INSIDE it as `children`, never as a sibling rendered over it.
110
137
  */
111
- export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
112
- {
113
- onPress,
114
- disabled = false,
115
- blurIntensity = BACKDROP_BLUR_INTENSITY,
116
- blurTint = 'dark',
117
- dimColor = '#000',
118
- dimOpacity = BACKDROP_DIM_OPACITY,
119
- style,
120
- children,
121
- accessibilityLabel = 'Dismiss',
122
- testID,
123
- },
124
- ref,
125
- ) {
138
+ export const Backdrop = memo(function Backdrop({
139
+ onPress,
140
+ progress,
141
+ disabled = false,
142
+ blurIntensity = BACKDROP_BLUR_INTENSITY,
143
+ blurTint = 'dark',
144
+ dimColor = '#000',
145
+ dimOpacity = BACKDROP_DIM_OPACITY,
146
+ style,
147
+ layerStyle,
148
+ children,
149
+ accessibilityLabel = 'Dismiss',
150
+ testID,
151
+ }: BackdropProps) {
126
152
  const inert = disabled || !onPress;
153
+
154
+ // An `opacity` handed in through `style` would sit on the ancestor of the
155
+ // blur and kill it; hoist it onto the layers instead of honouring it there.
156
+ const flat: ViewStyle = StyleSheet.flatten(style) ?? {};
157
+ const { opacity: styleOpacity, ...rootStyle } = flat;
158
+ const staticOpacity = typeof styleOpacity === 'number' ? styleOpacity : 1;
159
+
160
+ // The fade lives on each LAYER, never on their shared ancestor.
161
+ const blurFade = useAnimatedStyle(
162
+ () => ({ opacity: (progress ? progress.value : 1) * staticOpacity }),
163
+ [progress, staticOpacity],
164
+ );
165
+ const dimFade = useAnimatedStyle(
166
+ () => ({ opacity: (progress ? progress.value : 1) * staticOpacity * dimOpacity }),
167
+ [progress, staticOpacity, dimOpacity],
168
+ );
169
+
127
170
  return (
128
171
  <Pressable
129
- // MUST forward the ref: surfaces animate the backdrop through
130
- // `Animated.createAnimatedComponent(Backdrop)`, and reanimated applies
131
- // those updates by reaching the underlying host view. With the ref
132
- // swallowed the animated style never lands, so a backdrop whose opacity
133
- // is animated from 0 stays at 0 — fully transparent, with the surface
134
- // floating over an undimmed app.
135
- ref={ref}
136
172
  pointerEvents="auto"
137
173
  onPress={inert ? undefined : onPress}
138
174
  disabled={inert}
@@ -143,27 +179,27 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
143
179
  accessibilityRole={inert ? undefined : 'button'}
144
180
  accessibilityLabel={inert ? undefined : accessibilityLabel}
145
181
  testID={testID}
146
- style={[StyleSheet.absoluteFill, style]}
182
+ style={[StyleSheet.absoluteFill, rootStyle]}
147
183
  >
148
184
  {blurIntensity > 0 ? (
149
- <BlurView
185
+ <AnimatedBlurView
150
186
  intensity={blurIntensity}
151
187
  tint={blurTint}
152
188
  // Android's default blur is a no-op on many devices; this is the
153
189
  // implementation that actually renders there.
154
190
  experimentalBlurMethod="dimezisBlurView"
155
191
  pointerEvents="none"
156
- style={StyleSheet.absoluteFill}
192
+ style={[StyleSheet.absoluteFill, layerStyle, blurFade]}
157
193
  />
158
194
  ) : null}
159
- <View
195
+ <Animated.View
160
196
  pointerEvents="none"
161
- style={[StyleSheet.absoluteFill, { backgroundColor: dimColor, opacity: dimOpacity }]}
197
+ style={[StyleSheet.absoluteFill, { backgroundColor: dimColor }, layerStyle, dimFade]}
162
198
  />
163
199
  {children}
164
200
  </Pressable>
165
201
  );
166
- }));
202
+ });
167
203
 
168
204
  Backdrop.displayName = 'Backdrop';
169
205
 
@@ -74,8 +74,6 @@ import type {
74
74
  } from './types';
75
75
 
76
76
  const AnimatedImage = Animated.createAnimatedComponent(Image);
77
- // The shared overlay backdrop, driven by the viewer's open/drag progress.
78
- const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
79
77
 
80
78
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
81
79
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
@@ -791,11 +789,14 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
791
789
  did nothing on web while Escape still closed. `Backdrop` opts back
792
790
  in via the `pointerEvents` PROP, the only form that reaches the DOM
793
791
  (see `src/overlay`). */}
794
- <AnimatedBackdrop
792
+ {/* `progress` rather than an animated style: an opacity on the
793
+ backdrop's root would sit ABOVE the blur layer and neutralise it —
794
+ `backdrop-filter` samples nothing under an ancestor that composites
795
+ in isolation. */}
796
+ <Backdrop
795
797
  onPress={handleDismiss}
796
798
  accessibilityLabel="Close image viewer"
797
- dimColor={theme.colors.overlay}
798
- style={backdropStyle}
799
+ progress={opacity}
799
800
  />
800
801
 
801
802
  <GestureDetector gesture={panGesture}>