@oxyhq/bloom 1.1.0 → 1.2.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 (150) hide show
  1. package/docs/media-flight.mdx +272 -0
  2. package/docs/migrating-the-media-gallery.mdx +103 -0
  3. package/docs/migrating-to-1.0.mdx +1 -1
  4. package/docs/zoomable-media-gallery.mdx +116 -0
  5. package/lib/commonjs/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/index.web.js.map +1 -1
  8. package/lib/commonjs/media-flight/MediaFlightLayer.js +151 -0
  9. package/lib/commonjs/media-flight/MediaFlightLayer.js.map +1 -0
  10. package/lib/commonjs/media-flight/MediaSurface.js +188 -0
  11. package/lib/commonjs/media-flight/MediaSurface.js.map +1 -0
  12. package/lib/commonjs/media-flight/constants.js +39 -0
  13. package/lib/commonjs/media-flight/constants.js.map +1 -0
  14. package/lib/commonjs/media-flight/expo-video-module.js +148 -0
  15. package/lib/commonjs/media-flight/expo-video-module.js.map +1 -0
  16. package/lib/commonjs/media-flight/index.js +102 -0
  17. package/lib/commonjs/media-flight/index.js.map +1 -0
  18. package/lib/commonjs/media-flight/store.js +457 -0
  19. package/lib/commonjs/media-flight/store.js.map +1 -0
  20. package/lib/commonjs/media-flight/types.js +6 -0
  21. package/lib/commonjs/media-flight/types.js.map +1 -0
  22. package/lib/commonjs/media-flight/use-media-flight.js +41 -0
  23. package/lib/commonjs/media-flight/use-media-flight.js.map +1 -0
  24. package/lib/commonjs/tab-bar/TabBarBase.js +1 -1
  25. package/lib/commonjs/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +185 -131
  26. package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
  27. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
  28. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js +5 -5
  29. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js.map +1 -1
  30. package/lib/commonjs/zoomable-media-gallery/types.js +6 -0
  31. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
  32. package/lib/module/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
  33. package/lib/module/index.js +2 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/index.web.js +2 -1
  36. package/lib/module/index.web.js.map +1 -1
  37. package/lib/module/media-flight/MediaFlightLayer.js +146 -0
  38. package/lib/module/media-flight/MediaFlightLayer.js.map +1 -0
  39. package/lib/module/media-flight/MediaSurface.js +180 -0
  40. package/lib/module/media-flight/MediaSurface.js.map +1 -0
  41. package/lib/module/media-flight/constants.js +35 -0
  42. package/lib/module/media-flight/constants.js.map +1 -0
  43. package/lib/module/media-flight/expo-video-module.js +142 -0
  44. package/lib/module/media-flight/expo-video-module.js.map +1 -0
  45. package/lib/module/media-flight/index.js +9 -0
  46. package/lib/module/media-flight/index.js.map +1 -0
  47. package/lib/module/media-flight/store.js +439 -0
  48. package/lib/module/media-flight/store.js.map +1 -0
  49. package/lib/module/media-flight/types.js +4 -0
  50. package/lib/module/media-flight/types.js.map +1 -0
  51. package/lib/module/media-flight/use-media-flight.js +36 -0
  52. package/lib/module/media-flight/use-media-flight.js.map +1 -0
  53. package/lib/module/tab-bar/TabBarBase.js +1 -1
  54. package/lib/module/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +184 -130
  55. package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
  56. package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
  57. package/lib/module/zoomable-media-gallery/index.js +5 -0
  58. package/lib/module/zoomable-media-gallery/index.js.map +1 -0
  59. package/lib/module/zoomable-media-gallery/types.js +4 -0
  60. package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts +11 -0
  64. package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts +106 -0
  66. package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/media-flight/constants.d.ts +18 -0
  68. package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts +103 -0
  70. package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/media-flight/index.d.ts +9 -0
  72. package/lib/typescript/commonjs/media-flight/index.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/media-flight/store.d.ts +132 -0
  74. package/lib/typescript/commonjs/media-flight/store.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/media-flight/types.d.ts +195 -0
  76. package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts +4 -0
  78. package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
  80. package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/zoomable-media-gallery/index.d.ts +4 -0
  83. package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts +90 -0
  85. package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts.map +1 -0
  86. package/lib/typescript/module/index.d.ts.map +1 -1
  87. package/lib/typescript/module/index.web.d.ts.map +1 -1
  88. package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts +11 -0
  89. package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts.map +1 -0
  90. package/lib/typescript/module/media-flight/MediaSurface.d.ts +106 -0
  91. package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -0
  92. package/lib/typescript/module/media-flight/constants.d.ts +18 -0
  93. package/lib/typescript/module/media-flight/constants.d.ts.map +1 -0
  94. package/lib/typescript/module/media-flight/expo-video-module.d.ts +103 -0
  95. package/lib/typescript/module/media-flight/expo-video-module.d.ts.map +1 -0
  96. package/lib/typescript/module/media-flight/index.d.ts +9 -0
  97. package/lib/typescript/module/media-flight/index.d.ts.map +1 -0
  98. package/lib/typescript/module/media-flight/store.d.ts +132 -0
  99. package/lib/typescript/module/media-flight/store.d.ts.map +1 -0
  100. package/lib/typescript/module/media-flight/types.d.ts +195 -0
  101. package/lib/typescript/module/media-flight/types.d.ts.map +1 -0
  102. package/lib/typescript/module/media-flight/use-media-flight.d.ts +4 -0
  103. package/lib/typescript/module/media-flight/use-media-flight.d.ts.map +1 -0
  104. package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
  105. package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
  106. package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
  107. package/lib/typescript/module/zoomable-media-gallery/index.d.ts +4 -0
  108. package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
  109. package/lib/typescript/module/zoomable-media-gallery/types.d.ts +90 -0
  110. package/lib/typescript/module/zoomable-media-gallery/types.d.ts.map +1 -0
  111. package/package.json +42 -8
  112. package/src/image-aspect-ratio-cache/aspect-ratio-cache.ts +1 -1
  113. package/src/index.ts +2 -1
  114. package/src/index.web.ts +2 -1
  115. package/src/media-flight/MediaFlightLayer.tsx +144 -0
  116. package/src/media-flight/MediaSurface.tsx +233 -0
  117. package/src/media-flight/constants.ts +32 -0
  118. package/src/media-flight/expo-video-module.ts +181 -0
  119. package/src/media-flight/index.ts +33 -0
  120. package/src/media-flight/store.ts +497 -0
  121. package/src/media-flight/types.ts +210 -0
  122. package/src/media-flight/use-media-flight.ts +42 -0
  123. package/src/tab-bar/TabBarBase.tsx +1 -1
  124. package/src/{zoomable-image-gallery/ZoomableImageGallery.tsx → zoomable-media-gallery/ZoomableMediaGallery.tsx} +192 -117
  125. package/src/zoomable-media-gallery/index.ts +11 -0
  126. package/src/zoomable-media-gallery/types.ts +100 -0
  127. package/docs/zoomable-image-gallery.mdx +0 -66
  128. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
  129. package/lib/commonjs/zoomable-image-gallery/types.js +0 -2
  130. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
  131. package/lib/module/zoomable-image-gallery/index.js +0 -5
  132. package/lib/module/zoomable-image-gallery/index.js.map +0 -1
  133. package/lib/module/zoomable-image-gallery/types.js +0 -2
  134. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
  135. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
  136. package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +0 -4
  137. package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +0 -56
  138. package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +0 -1
  139. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
  140. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
  141. package/lib/typescript/module/zoomable-image-gallery/index.d.ts +0 -4
  142. package/lib/typescript/module/zoomable-image-gallery/types.d.ts +0 -56
  143. package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +0 -1
  144. package/src/zoomable-image-gallery/index.ts +0 -9
  145. package/src/zoomable-image-gallery/types.ts +0 -59
  146. /package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
  147. /package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
  148. /package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
  149. /package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
  150. /package/src/{zoomable-image-gallery → zoomable-media-gallery}/constants.ts +0 -0
@@ -3,10 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.ZoomableImageGallery = void 0;
6
+ exports.default = exports.ZoomableMediaGallery = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
- var _expoImage = require("expo-image");
10
9
  var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
11
10
  var _reactNativeGestureHandler = require("react-native-gesture-handler");
12
11
  var _tokens = require("../styles/tokens.js");
@@ -15,11 +14,43 @@ var _portal = require("../portal");
15
14
  var _useInteractionState = require("../hooks/use-interaction-state.js");
16
15
  var _index2 = require("../icons/index.js");
17
16
  var _webViewStyle = require("../styles/web-view-style.js");
17
+ var _MediaSurface = require("../media-flight/MediaSurface.js");
18
18
  var _index3 = require("../image-aspect-ratio-cache/index.js");
19
19
  var _constants = require("./constants.js");
20
20
  var _jsxRuntime = require("react/jsx-runtime");
21
21
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
22
- const AnimatedImage = _reactNativeReanimated.default.createAnimatedComponent(_expoImage.Image);
22
+ /**
23
+ * The still-frame URI of an item: the image itself, or a video's poster.
24
+ * `undefined` for a video with no poster — there is nothing to probe, size
25
+ * against or show in the strip, and every caller below handles that.
26
+ */
27
+ function posterUri(item) {
28
+ return item.kind === 'video' ? item.poster : item.uri;
29
+ }
30
+
31
+ /** The aspect ratio already known for an item, from the shared probe cache. */
32
+ function cachedRatio(item) {
33
+ const uri = posterUri(item);
34
+ return uri === undefined ? undefined : (0, _index3.getAspectRatio)(uri);
35
+ }
36
+
37
+ /**
38
+ * A stable React key. An image is identified by its URI; a video is not (its
39
+ * player is an object with no URL), which is why `GalleryVideo` carries an
40
+ * explicit `id`. The index is folded in so a post repeating one image still
41
+ * pages correctly.
42
+ */
43
+ function mediaKey(item, index) {
44
+ return item.kind === 'video' ? `video-${item.id}-${index}` : `image-${item.uri}-${index}`;
45
+ }
46
+
47
+ /**
48
+ * What the share sheet is handed. A poster is not what "share this video"
49
+ * means, so a video shares its own `shareUrl` or nothing at all.
50
+ */
51
+ function shareUri(item) {
52
+ return item.kind === 'video' ? item.shareUrl : item.uri;
53
+ }
23
54
 
24
55
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
25
56
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
@@ -62,7 +93,7 @@ function NavArrow({
62
93
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
63
94
  onPress: onPress,
64
95
  accessibilityRole: "button",
65
- accessibilityLabel: isLeft ? 'Previous image' : 'Next image',
96
+ accessibilityLabel: isLeft ? 'Previous item' : 'Next item',
66
97
  hitSlop: 8,
67
98
  onHoverIn: onHoverIn,
68
99
  onHoverOut: onHoverOut,
@@ -77,7 +108,7 @@ function NavArrow({
77
108
  });
78
109
  }
79
110
  /**
80
- * Resolve `cornerRadius` against the size an image is actually rendered at, so
111
+ * Resolve `cornerRadius` against the size an item is actually rendered at, so
81
112
  * `'circle'` stays a circle at every fitted size (and through the open/close
82
113
  * animation, which scales this same box).
83
114
  */
@@ -86,26 +117,42 @@ function resolveCornerRadius(cornerRadius, fit) {
86
117
  }
87
118
 
88
119
  /**
89
- * Fullscreen, swipeable image viewer that replicates the profile avatar's
90
- * measured-origin zoom transition (`ZoomableAvatar`) for rectangular post media:
120
+ * Fullscreen, swipeable MEDIA viewer images and consumer-owned videos in one
121
+ * pager — replicating the profile avatar's measured-origin zoom transition
122
+ * (`ZoomableAvatar`) for rectangular post media:
91
123
  *
92
124
  * - Open/close feel is identical to the avatar (same spring configs, web
93
125
  * timing/easing, blur backdrop, and the measure-origin technique). The viewer
94
126
  * renders through the Bloom `Portal` on BOTH platforms (RN's `Modal` is not
95
127
  * used on native — on the New Architecture / Fabric Android its host views
96
128
  * mount full-screen but never composite, leaving the viewer invisible).
97
- * - The OPENING (tapped) image animates from its measured rect to a centered,
129
+ * - The OPENING (tapped) item animates from its measured rect to a centered,
98
130
  * aspect-ratio-preserving fit within {@link FIT_FRACTION} of the screen. Once
99
131
  * the open animation settles, a horizontal paging `ScrollView` mounts seeded at
100
- * the tapped index so the user can swipe between every image in the post.
132
+ * the tapped index so the user can swipe between every item in the post.
101
133
  * - Gesture disambiguation: the pager owns horizontal swipes; a vertical-only
102
134
  * `Gesture.Pan` (`activeOffsetY` + `failOffsetX`) owns drag-to-dismiss, so the
103
135
  * two never fight.
136
+ *
137
+ * ## Video, and why it does not restart
138
+ *
139
+ * A video page is fed by a `VideoPlayer` the CONSUMER created and owns. Bloom
140
+ * never creates or destroys one: expo-video keeps the player OBJECT separate
141
+ * from the `VideoView` that shows it, and one player may feed several views, so
142
+ * handing the same player to this gallery moves a playing video into fullscreen
143
+ * without re-opening the stream. Exactly ONE view is mounted per opening — the
144
+ * active page's — while every other page and every strip tile renders the still
145
+ * (`MediaPoster`). The single unavoidable swap is the pre-pager open surface
146
+ * handing over to the pager's active page once the open animation settles.
147
+ *
148
+ * `expo-video` is an OPTIONAL peer loaded through `media-flight/expo-video-module`.
149
+ * Without it a video page degrades to its poster, once, with a dev warning.
104
150
  */
105
- const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
151
+ const ZoomableMediaGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
106
152
  measureThumb,
107
153
  cornerRadius = _constants.DEFAULT_CORNER_RADIUS,
108
- indicatorVariant = 'dots'
154
+ indicatorVariant = 'dots',
155
+ videoControls = false
109
156
  }, ref) => {
110
157
  const {
111
158
  width: SCREEN_WIDTH,
@@ -113,17 +160,17 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
113
160
  } = (0, _reactNative.useWindowDimensions)();
114
161
  const radiusFor = (0, _react.useCallback)(fit => resolveCornerRadius(cornerRadius, fit), [cornerRadius]);
115
162
  const [isOpen, setIsOpen] = (0, _react.useState)(false);
116
- // Once true, the swipeable pager is mounted and the single open-image hidden.
163
+ // Once true, the swipeable pager is mounted and the single open-surface hidden.
117
164
  const [pagerReady, setPagerReady] = (0, _react.useState)(false);
118
- const [images, setImages] = (0, _react.useState)([]);
165
+ const [items, setItems] = (0, _react.useState)([]);
119
166
  const [activeIndex, setActiveIndex] = (0, _react.useState)(0);
120
- // Whether the active image is currently pinched/double-tapped past its base
167
+ // Whether the active media is currently pinched/double-tapped past its base
121
168
  // size. Drives the pan-while-zoomed gesture, disables the pager's horizontal
122
- // paging + the dismiss drag so a pan moves the zoomed image instead.
169
+ // paging + the dismiss drag so a pan moves the zoomed media instead.
123
170
  const [isZoomed, setIsZoomed] = (0, _react.useState)(false);
124
- // Aspect ratio of the OPENING image (drives the open-animation fit box).
171
+ // Aspect ratio of the OPENING item (drives the open-animation fit box).
125
172
  const [openRatio, setOpenRatio] = (0, _react.useState)(_index3.DEFAULT_ASPECT_RATIO);
126
- // Per-image aspect ratios for the pager pages (index-aligned with `images`).
173
+ // Per-item aspect ratios for the pager pages (index-aligned with `items`).
127
174
  const [pageRatios, setPageRatios] = (0, _react.useState)({});
128
175
 
129
176
  // Native `Share` is always available; web exposes a share button only when the
@@ -137,7 +184,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
137
184
  const translateY = (0, _reactNativeReanimated.useSharedValue)(0);
138
185
  const opacity = (0, _reactNativeReanimated.useSharedValue)(0);
139
186
 
140
- // Zoom transform of the active image, kept SEPARATE from the open/close +
187
+ // Zoom transform of the active media, kept SEPARATE from the open/close +
141
188
  // dismiss-drag transform above (reusing those would collide two meanings).
142
189
  // `zoom*` are the live/persisted values; `baseZoomScale` is captured at pinch
143
190
  // start and `savedZoomTranslate*` at pan start for incremental bookkeeping.
@@ -148,14 +195,14 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
148
195
  const savedZoomTranslateX = (0, _reactNativeReanimated.useSharedValue)(0);
149
196
  const savedZoomTranslateY = (0, _reactNativeReanimated.useSharedValue)(0);
150
197
 
151
- // Origin (offset from screen center) of the tapped image.
198
+ // Origin (offset from screen center) of the tapped item.
152
199
  const originX = (0, _reactNativeReanimated.useSharedValue)(0);
153
200
  const originY = (0, _reactNativeReanimated.useSharedValue)(0);
154
201
  const startX = (0, _reactNativeReanimated.useSharedValue)(0);
155
202
  const startY = (0, _reactNativeReanimated.useSharedValue)(0);
156
203
  // Per-drag baseline captured at pan start (transient).
157
204
  const startScale = (0, _reactNativeReanimated.useSharedValue)(1);
158
- // Persistent scale of the open-image at its thumbnail origin (thumbWidth /
205
+ // Persistent scale of the open-surface at its thumbnail origin (thumbWidth /
159
206
  // fittedWidth). The open animation grows from this to 1; the close animation
160
207
  // shrinks back to it. Never overwritten by the drag gesture.
161
208
  const originScale = (0, _reactNativeReanimated.useSharedValue)(1);
@@ -210,9 +257,9 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
210
257
  setActiveIndex(prev => prev === next ? prev : next);
211
258
  }, []);
212
259
 
213
- // Snap the active image back to its un-zoomed baseline (no animation) and clear
214
- // all zoom bookkeeping. Called on every path that changes the active page so a
215
- // freshly-viewed image always starts un-zoomed.
260
+ // Snap the active media back to its un-zoomed baseline (no animation) and
261
+ // clear all zoom bookkeeping. Called on every path that changes the active
262
+ // page so a freshly-viewed item always starts un-zoomed.
216
263
  const resetZoom = (0, _react.useCallback)(() => {
217
264
  zoomScale.value = 1;
218
265
  zoomTranslateX.value = 0;
@@ -230,7 +277,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
230
277
  }), [SCREEN_WIDTH, SCREEN_HEIGHT]);
231
278
 
232
279
  // Largest width/height for `ratio` that fits inside `fitBox` without cropping
233
- // (contain), never exceeding the image's own resolution: filling the viewport
280
+ // (contain), never exceeding the media's own resolution: filling the viewport
234
281
  // with a 512px avatar or a small thumbnail just renders it soft. `uri` is what
235
282
  // lets us look the intrinsic size up; without it (not measured yet) the fit
236
283
  // box wins and the clamp applies on the next frame.
@@ -253,18 +300,24 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
253
300
  };
254
301
  }, [fitBox]);
255
302
 
256
- // Fitted size of the single open-image for the CURRENT page. On open this is
257
- // the opened image's fit (active index == opened index, ratio == `openRatio`);
258
- // after swiping it tracks the viewed image so the collapse-on-dismiss renders
259
- // and flies back the image actually on screen.
303
+ // Fitted size of the single open-surface for the CURRENT page. On open this is
304
+ // the opened item's fit (active index == opened index, ratio == `openRatio`);
305
+ // after swiping it tracks the viewed item so the collapse-on-dismiss renders
306
+ // and flies back the media actually on screen.
307
+ const activeItem = items[activeIndex];
260
308
  const activeRatio = pageRatios[activeIndex] ?? openRatio;
261
- const activeUri = images[activeIndex]?.uri;
309
+ const activeUri = activeItem === undefined ? undefined : posterUri(activeItem);
262
310
  const activeFit = (0, _react.useMemo)(() => fitForRatio(activeRatio, activeUri), [fitForRatio, activeRatio, activeUri]);
263
311
 
264
- // Alt text (accessibility description) of the image currently on screen, shown
312
+ // Alt text (accessibility description) of the item currently on screen, shown
265
313
  // as a caption at the bottom of the viewer (Bluesky-style lightbox footer).
266
- const activeAlt = images[activeIndex]?.alt?.trim() || undefined;
314
+ const activeAlt = activeItem?.alt?.trim() || undefined;
315
+
316
+ // Probe and cache the ratio of the page at `index`. A video with no poster has
317
+ // nothing to probe — it keeps whatever ratio the consumer declared, or the
318
+ // gallery default — so this is a no-op rather than a guess.
267
319
  const ensureRatio = (0, _react.useCallback)((index, uri) => {
320
+ if (uri === undefined) return;
268
321
  const cached = (0, _index3.getAspectRatio)(uri);
269
322
  if (cached !== undefined) {
270
323
  setPageRatios(prev => prev[index] === cached ? prev : {
@@ -292,7 +345,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
292
345
  opacity.value = 0;
293
346
  }, [opacity, scale, translateX, translateY]);
294
347
 
295
- // Fly the (possibly dragged) image back toward `target` — the rect of the
348
+ // Fly the (possibly dragged) media back toward `target` — the rect of the
296
349
  // thumbnail currently being viewed — shrinking to its footprint. Uses the
297
350
  // EXACT same close spring (native) / timing (web) as the avatar transition.
298
351
  const flyBackTo = (0, _react.useCallback)(target => {
@@ -358,13 +411,13 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
358
411
  // fly-back from an already-moving image.
359
412
  if (dismissingRef.current) return;
360
413
  dismissingRef.current = true;
361
- // Collapse the pager back to the single open-image so the fly-back animates
362
- // one image (the current one) rather than the whole scrolled strip.
414
+ // Collapse the pager back to the single open-surface so the fly-back
415
+ // animates one item (the current one) rather than the whole scrolled strip.
363
416
  setPagerReady(false);
364
417
  const index = activeIndexRef.current;
365
- const current = images[index];
366
- // Recompute the fly-back target from the CURRENT image: the live rect of its
367
- // thumbnail + the same fitted box (`activeFit`) the open-image is rendered
418
+ const current = items[index];
419
+ // Recompute the fly-back target from the CURRENT item: the live rect of its
420
+ // thumbnail + the same fitted box (`activeFit`) the open-surface is rendered
368
421
  // at, using the same screen-center math as `open`. Falls back to a center
369
422
  // fade-out when the thumbnail can't be measured.
370
423
  if (measureThumb && current) {
@@ -384,7 +437,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
384
437
  return;
385
438
  }
386
439
  fadeOutCenter();
387
- }, [activeFit, fadeOutCenter, flyBackTo, images, measureThumb, SCREEN_HEIGHT, SCREEN_WIDTH]);
440
+ }, [activeFit, fadeOutCenter, flyBackTo, items, measureThumb, SCREEN_HEIGHT, SCREEN_WIDTH]);
388
441
 
389
442
  // Reveal the swipeable pager once the open animation has settled. The index it
390
443
  // lands on is `activeIndexRef` (set synchronously in `open`), seated by the
@@ -392,30 +445,32 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
392
445
  const revealPager = (0, _react.useCallback)(() => {
393
446
  setPagerReady(true);
394
447
  }, []);
395
- const open = (0, _react.useCallback)((nextImages, index, rect) => {
396
- if (isOpen || nextImages.length === 0) return;
448
+ const open = (0, _react.useCallback)((nextItems, index, rect) => {
449
+ if (isOpen || nextItems.length === 0) return;
397
450
  dismissingRef.current = false;
398
- const safeIndex = Math.min(Math.max(index, 0), nextImages.length - 1);
399
- const target = nextImages[safeIndex];
451
+ const safeIndex = Math.min(Math.max(index, 0), nextItems.length - 1);
452
+ const target = nextItems[safeIndex];
400
453
  if (!target) return;
401
- const knownRatio = target.aspectRatio ?? (0, _index3.getAspectRatio)(target.uri);
454
+ const targetUri = posterUri(target);
455
+ const knownRatio = target.aspectRatio ?? cachedRatio(target);
402
456
  const ratio = knownRatio ?? _index3.DEFAULT_ASPECT_RATIO;
403
- setImages(nextImages);
457
+ setItems(nextItems);
404
458
  setActiveIndexBoth(safeIndex);
405
459
  setOpenRatio(ratio);
406
460
  // Seed every page whose ratio is already known (consumer-provided metadata
407
461
  // or a previously-cached probe) so swiping never hits the same snap — only
408
- // a genuinely-unknown image falls through to `ensureRatio`'s async probe.
462
+ // a genuinely-unknown item falls through to `ensureRatio`'s async probe.
409
463
  const initialRatios = {};
410
- nextImages.forEach((img, i) => {
411
- const r = img.aspectRatio ?? (0, _index3.getAspectRatio)(img.uri);
464
+ nextItems.forEach((item, i) => {
465
+ const r = item.aspectRatio ?? cachedRatio(item);
412
466
  if (r !== undefined) initialRatios[i] = r;
413
467
  });
414
468
  setPageRatios(initialRatios);
415
469
 
416
- // Resolve the opening ratio if it was not yet known, then re-fit.
417
- if (knownRatio === undefined) {
418
- void (0, _index3.fetchAspectRatio)(target.uri).then(resolved => {
470
+ // Resolve the opening ratio if it was not yet known, then re-fit. A video
471
+ // with no poster has nothing to probe and keeps the default.
472
+ if (knownRatio === undefined && targetUri !== undefined) {
473
+ void (0, _index3.fetchAspectRatio)(targetUri).then(resolved => {
419
474
  setOpenRatio(resolved);
420
475
  setPageRatios(prev => ({
421
476
  ...prev,
@@ -424,11 +479,11 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
424
479
  });
425
480
  }
426
481
 
427
- // The open-image box is rendered at its FINAL fitted size; the open
482
+ // The open-surface box is rendered at its FINAL fitted size; the open
428
483
  // animation grows it from the thumbnail's footprint (scale < 1) up to 1,
429
484
  // mirroring the avatar's small→big scale. The initial scale is the ratio
430
485
  // of the thumbnail width to the fitted width.
431
- const fitted = fitForRatio(ratio, target.uri);
486
+ const fitted = fitForRatio(ratio, targetUri);
432
487
  const centerX = SCREEN_WIDTH / 2;
433
488
  const centerY = SCREEN_HEIGHT / 2;
434
489
  if (rect && rect.width > 0) {
@@ -528,7 +583,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
528
583
  opacity: opacity.value
529
584
  }), [opacity]);
530
585
 
531
- // The single open-image animates from origin → fitted center.
586
+ // The single open-surface animates from origin → fitted center.
532
587
  const openImageStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
533
588
  transform: [{
534
589
  translateX: translateX.value
@@ -568,7 +623,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
568
623
  // close fly-back target). Shared by `onMomentumScrollEnd` (native) and `onScroll`
569
624
  // (web, where paging may not fire a reliable momentum-end).
570
625
  const updateIndexFromOffset = (0, _react.useCallback)(offsetX => {
571
- const lastIndex = images.length - 1;
626
+ const lastIndex = items.length - 1;
572
627
  if (lastIndex < 0) return;
573
628
  const next = Math.min(Math.max(Math.round(offsetX / SCREEN_WIDTH), 0), lastIndex);
574
629
  if (next === activeIndexRef.current) return;
@@ -576,16 +631,16 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
576
631
  // the page scrolling away).
577
632
  resetZoom();
578
633
  setActiveIndexBoth(next);
579
- const img = images[next];
580
- if (img) ensureRatio(next, img.uri);
581
- }, [ensureRatio, images, resetZoom, setActiveIndexBoth, SCREEN_WIDTH]);
634
+ const item = items[next];
635
+ if (item) ensureRatio(next, posterUri(item));
636
+ }, [ensureRatio, items, resetZoom, setActiveIndexBoth, SCREEN_WIDTH]);
582
637
 
583
638
  // Programmatically page to `index` (arrow buttons, keyboard, thumbnail taps).
584
639
  // Clamps into range and lets the existing scroll handlers own the index/ratio
585
640
  // state-sync — this only triggers the scroll. No-op until the pager is mounted.
586
641
  const pageTo = (0, _react.useCallback)(index => {
587
642
  if (!pagerReady) return;
588
- const lastIndex = images.length - 1;
643
+ const lastIndex = items.length - 1;
589
644
  if (lastIndex < 0) return;
590
645
  const clamped = Math.min(Math.max(index, 0), lastIndex);
591
646
  resetZoom();
@@ -594,7 +649,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
594
649
  y: 0,
595
650
  animated: true
596
651
  });
597
- }, [images.length, pagerReady, resetZoom, SCREEN_WIDTH]);
652
+ }, [items.length, pagerReady, resetZoom, SCREEN_WIDTH]);
598
653
  const onPagerScroll = (0, _react.useCallback)(event => {
599
654
  updateIndexFromOffset(event.nativeEvent.contentOffset.x);
600
655
  }, [updateIndexFromOffset]);
@@ -715,10 +770,12 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
715
770
  // pinch + pan-while-zoomed.
716
771
  const activePageGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Simultaneous(tapGesture, pinchGesture, panWhileZoomedGesture), [tapGesture, pinchGesture, panWhileZoomedGesture]);
717
772
 
718
- // Share the active image's URI: the OS share sheet on native; the Web Share API
719
- // on supporting browsers (the button is only rendered where it exists).
773
+ // Share the active item's URI: the OS share sheet on native; the Web Share API
774
+ // on supporting browsers (the button is only rendered where it exists). A
775
+ // video shares its own `shareUrl`; its poster is not what "share this" means.
720
776
  const handleShare = (0, _react.useCallback)(async () => {
721
- const uri = images[activeIndexRef.current]?.uri;
777
+ const item = items[activeIndexRef.current];
778
+ const uri = item === undefined ? undefined : shareUri(item);
722
779
  if (!uri) return;
723
780
  try {
724
781
  if (_reactNative.Platform.OS === 'web') {
@@ -736,9 +793,9 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
736
793
  // A user-cancelled share sheet rejects (AbortError on web) — that's
737
794
  // expected, not a failure. Anything else is a real error worth surfacing.
738
795
  if (err instanceof Error && err.name === 'AbortError') return;
739
- if (typeof console !== 'undefined' && console.error) console.error('Image share failed:', err);
796
+ if (typeof console !== 'undefined' && console.error) console.error('Media share failed:', err);
740
797
  }
741
- }, [images]);
798
+ }, [items]);
742
799
 
743
800
  // Keyboard navigation (web only), scoped to the open lifetime like Dialog.web's
744
801
  // Escape handler. Reads the live index from the ref so it need not re-subscribe
@@ -766,7 +823,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
766
823
  style: _reactNative.StyleSheet.absoluteFill,
767
824
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Backdrop, {
768
825
  onPress: handleDismiss,
769
- accessibilityLabel: "Close image viewer",
826
+ accessibilityLabel: "Close media viewer",
770
827
  progress: opacity
771
828
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
772
829
  gesture: panGesture,
@@ -776,23 +833,22 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
776
833
  height: SCREEN_HEIGHT
777
834
  }, webUserSelectNoneStyle],
778
835
  pointerEvents: "box-none",
779
- children: [!pagerReady && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
836
+ children: [!pagerReady && activeItem !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
780
837
  onPress: handleDismiss,
781
838
  style: webPointerStyle,
782
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedImage, {
783
- source: {
784
- uri: images[activeIndex]?.uri
785
- },
786
- contentFit: "contain",
787
- style: [{
839
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
840
+ style: [styles.mediaBox, {
788
841
  width: activeFit.width,
789
842
  height: activeFit.height,
790
843
  borderRadius: radiusFor(activeFit)
791
844
  }, openImageStyle],
792
- transition: 0,
793
- ...(_reactNative.Platform.OS === 'web' ? {
794
- draggable: false
795
- } : {})
845
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSurface.MediaSurface, {
846
+ content: activeItem,
847
+ contentFit: "contain",
848
+ nativeControls: videoControls,
849
+ accessibilityLabel: activeItem.alt,
850
+ style: _reactNative.StyleSheet.absoluteFill
851
+ })
796
852
  })
797
853
  }), pagerReady && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
798
854
  style: [_reactNative.StyleSheet.absoluteFill, pagerContainerStyle],
@@ -815,73 +871,71 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
815
871
  } : {}),
816
872
  scrollEventThrottle: 16,
817
873
  style: _reactNative.StyleSheet.absoluteFill,
818
- children: images.map((img, idx) => {
819
- const ratio = pageRatios[idx] ?? (0, _index3.getAspectRatio)(img.uri) ?? _index3.DEFAULT_ASPECT_RATIO;
820
- const fit = fitForRatio(ratio, img.uri);
874
+ children: items.map((item, idx) => {
875
+ const uri = posterUri(item);
876
+ const ratio = pageRatios[idx] ?? cachedRatio(item) ?? item.aspectRatio ?? _index3.DEFAULT_ASPECT_RATIO;
877
+ const fit = fitForRatio(ratio, uri);
821
878
  // Only the active page is zoomable + gesture-wrapped (double-tap
822
- // / single-tap dismiss / pinch / pan). Off-screen pages stay a
823
- // plain image; the active image handles its own tap-to-dismiss
824
- // through the gesture, so it isn't wrapped in a Pressable.
879
+ // / single-tap dismiss / pinch / pan). It is also the ONLY page
880
+ // that mounts a real media surface: an off-screen page renders
881
+ // the still, because a second `VideoView` on the same player is
882
+ // exactly the duplicate surface this package exists to avoid.
883
+ // The active media handles its own tap-to-dismiss through the
884
+ // gesture, so it isn't wrapped in a Pressable.
825
885
  if (idx === activeIndex) {
826
886
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
827
887
  // The page fills the screen ON TOP of the backdrop (the
828
888
  // pager below it needs pointer events to swipe), so a
829
- // press on the empty area around the image can never
889
+ // press on the empty area around the media can never
830
890
  // reach the backdrop — the page dismisses it itself.
831
- // Same outcome as tapping the image, which already
891
+ // Same outcome as tapping the media, which already
832
892
  // dismisses through `singleTapDismissGesture`.
833
893
  onPress: handleDismiss,
834
- accessibilityLabel: "Close image viewer",
894
+ accessibilityLabel: "Close media viewer",
835
895
  style: [styles.page, {
836
896
  width: SCREEN_WIDTH,
837
897
  height: SCREEN_HEIGHT
838
898
  }, webPointerStyle],
839
899
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
840
900
  gesture: activePageGesture,
841
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedImage, {
842
- source: {
843
- uri: img.uri
844
- },
845
- contentFit: "contain",
846
- style: [{
901
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
902
+ style: [styles.mediaBox, {
847
903
  width: fit.width,
848
904
  height: fit.height,
849
905
  borderRadius: radiusFor(fit)
850
906
  }, zoomImageStyle],
851
- transition: 0,
852
- ...(_reactNative.Platform.OS === 'web' ? {
853
- draggable: false
854
- } : {})
907
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSurface.MediaSurface, {
908
+ content: item,
909
+ contentFit: "contain",
910
+ nativeControls: videoControls,
911
+ accessibilityLabel: item.alt,
912
+ style: _reactNative.StyleSheet.absoluteFill
913
+ })
855
914
  })
856
915
  })
857
- }, `${img.uri}-${idx}`);
916
+ }, mediaKey(item, idx));
858
917
  }
859
918
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
860
919
  onPress: handleDismiss,
861
- accessibilityLabel: "Close image viewer",
920
+ accessibilityLabel: "Close media viewer",
862
921
  style: [styles.page, {
863
922
  width: SCREEN_WIDTH,
864
923
  height: SCREEN_HEIGHT
865
924
  }],
866
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_expoImage.Image, {
867
- source: {
868
- uri: img.uri
869
- },
925
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSurface.MediaPoster, {
926
+ content: item,
870
927
  contentFit: "contain",
928
+ accessibilityLabel: item.alt,
871
929
  style: {
872
930
  width: fit.width,
873
931
  height: fit.height,
874
932
  borderRadius: radiusFor(fit)
875
- },
876
- transition: 0,
877
- ...(_reactNative.Platform.OS === 'web' ? {
878
- draggable: false
879
- } : {})
933
+ }
880
934
  })
881
- }, `${img.uri}-${idx}`);
935
+ }, mediaKey(item, idx));
882
936
  })
883
937
  })
884
- }), pagerReady && images.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
938
+ }), pagerReady && items.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
885
939
  style: [styles.indicatorWrap, backdropStyle],
886
940
  pointerEvents: "box-none",
887
941
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -889,47 +943,41 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
889
943
  pointerEvents: "none",
890
944
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
891
945
  style: styles.counterText,
892
- children: `${activeIndex + 1} / ${images.length}`
946
+ children: `${activeIndex + 1} / ${items.length}`
893
947
  })
894
948
  }), indicatorVariant === 'thumbnails' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
895
949
  horizontal: true,
896
950
  showsHorizontalScrollIndicator: false,
897
951
  style: styles.thumbStripScroll,
898
952
  contentContainerStyle: styles.thumbStripContent,
899
- children: images.map((img, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
953
+ children: items.map((item, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
900
954
  onPress: () => pageTo(idx),
901
955
  accessibilityRole: "button",
902
- accessibilityLabel: `Go to image ${idx + 1} of ${images.length}`,
956
+ accessibilityLabel: `Go to item ${idx + 1} of ${items.length}`,
903
957
  style: [styles.thumbTile, idx === activeIndex ? styles.thumbTileActive : styles.thumbTileInactive, webPointerStyle],
904
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_expoImage.Image, {
905
- source: {
906
- uri: img.uri
907
- },
958
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSurface.MediaPoster, {
959
+ content: item,
908
960
  contentFit: "cover",
909
- style: styles.thumbTileImage,
910
- transition: 0,
911
- ...(_reactNative.Platform.OS === 'web' ? {
912
- draggable: false
913
- } : {})
961
+ style: styles.thumbTileImage
914
962
  })
915
- }, `thumb-${img.uri}-${idx}`))
963
+ }, `thumb-${mediaKey(item, idx)}`))
916
964
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
917
965
  style: styles.dotsRow,
918
966
  pointerEvents: "none",
919
- children: images.map((img, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
967
+ children: items.map((item, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
920
968
  style: [styles.dot, idx === activeIndex ? styles.dotActive : styles.dotInactive]
921
- }, `dot-${img.uri}-${idx}`))
969
+ }, `dot-${mediaKey(item, idx)}`))
922
970
  })]
923
- }), _reactNative.Platform.OS === 'web' && pagerReady && images.length > 1 && activeIndex > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(NavArrow, {
971
+ }), _reactNative.Platform.OS === 'web' && pagerReady && items.length > 1 && activeIndex > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(NavArrow, {
924
972
  direction: "left",
925
973
  onPress: () => pageTo(activeIndex - 1)
926
- }), _reactNative.Platform.OS === 'web' && pagerReady && images.length > 1 && activeIndex < images.length - 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(NavArrow, {
974
+ }), _reactNative.Platform.OS === 'web' && pagerReady && items.length > 1 && activeIndex < items.length - 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(NavArrow, {
927
975
  direction: "right",
928
976
  onPress: () => pageTo(activeIndex + 1)
929
977
  }), canShare && pagerReady && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
930
978
  onPress: handleShare,
931
979
  accessibilityRole: "button",
932
- accessibilityLabel: "Share image",
980
+ accessibilityLabel: "Share media",
933
981
  hitSlop: 8,
934
982
  style: [styles.shareButton, webPointerStyle],
935
983
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.ArrowOutOfBox_Stroke2_Corner0_Rounded, {
@@ -937,7 +985,7 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
937
985
  size: "md"
938
986
  })
939
987
  }), activeAlt ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
940
- style: [styles.altCaptionWrap, images.length > 1 && styles.altCaptionWrapWithIndicator, backdropStyle],
988
+ style: [styles.altCaptionWrap, items.length > 1 && styles.altCaptionWrapWithIndicator, backdropStyle],
941
989
  pointerEvents: "none",
942
990
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
943
991
  style: styles.altCaptionPill,
@@ -964,8 +1012,8 @@ const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
964
1012
  children: renderContent()
965
1013
  });
966
1014
  });
967
- ZoomableImageGalleryInner.displayName = 'ZoomableImageGallery';
968
- const ZoomableImageGallery = exports.ZoomableImageGallery = ZoomableImageGalleryInner;
1015
+ ZoomableMediaGalleryInner.displayName = 'ZoomableMediaGallery';
1016
+ const ZoomableMediaGallery = exports.ZoomableMediaGallery = ZoomableMediaGalleryInner;
969
1017
  const styles = _reactNative.StyleSheet.create({
970
1018
  modalContainer: {
971
1019
  justifyContent: 'center',
@@ -1008,6 +1056,12 @@ const styles = _reactNative.StyleSheet.create({
1008
1056
  justifyContent: 'center',
1009
1057
  alignItems: 'center'
1010
1058
  },
1059
+ // The box the media is painted into. `overflow: hidden` is what makes the
1060
+ // box's corner radius clip a VIDEO — an image would round on its own, a
1061
+ // platform video surface will not.
1062
+ mediaBox: {
1063
+ overflow: 'hidden'
1064
+ },
1011
1065
  indicatorWrap: {
1012
1066
  position: 'absolute',
1013
1067
  bottom: 48,
@@ -1143,5 +1197,5 @@ const styles = _reactNative.StyleSheet.create({
1143
1197
  lineHeight: 18
1144
1198
  }
1145
1199
  });
1146
- var _default = exports.default = ZoomableImageGallery;
1147
- //# sourceMappingURL=ZoomableImageGallery.js.map
1200
+ var _default = exports.default = ZoomableMediaGallery;
1201
+ //# sourceMappingURL=ZoomableMediaGallery.js.map