react-native-image-stitcher 0.11.1 → 0.13.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 (37) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +28 -0
  3. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +3 -2
  4. package/dist/camera/ARCameraView.d.ts +10 -0
  5. package/dist/camera/ARCameraView.js +1 -0
  6. package/dist/camera/Camera.d.ts +191 -0
  7. package/dist/camera/Camera.js +250 -9
  8. package/dist/camera/OrientationDriftModal.d.ts +83 -0
  9. package/dist/camera/OrientationDriftModal.js +159 -0
  10. package/dist/camera/PanoramaBandOverlay.d.ts +13 -1
  11. package/dist/camera/PanoramaBandOverlay.js +106 -45
  12. package/dist/camera/PanoramaSettingsModal.js +15 -1
  13. package/dist/camera/ViewportCropOverlay.d.ts +35 -31
  14. package/dist/camera/ViewportCropOverlay.js +39 -30
  15. package/dist/camera/useDeviceOrientation.d.ts +18 -9
  16. package/dist/camera/useDeviceOrientation.js +18 -9
  17. package/dist/camera/useOrientationDrift.d.ts +104 -0
  18. package/dist/camera/useOrientationDrift.js +120 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.js +12 -1
  21. package/dist/stitching/incremental.d.ts +5 -3
  22. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +7 -1
  23. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +4 -3
  24. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +9 -7
  25. package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -7
  26. package/package.json +1 -1
  27. package/src/camera/ARCameraView.tsx +18 -1
  28. package/src/camera/Camera.tsx +639 -21
  29. package/src/camera/OrientationDriftModal.tsx +224 -0
  30. package/src/camera/PanoramaBandOverlay.tsx +135 -49
  31. package/src/camera/PanoramaSettingsModal.tsx +14 -0
  32. package/src/camera/ViewportCropOverlay.tsx +52 -30
  33. package/src/camera/__tests__/useOrientationDrift.test.ts +169 -0
  34. package/src/camera/useDeviceOrientation.ts +18 -9
  35. package/src/camera/useOrientationDrift.ts +172 -0
  36. package/src/index.ts +13 -0
  37. package/src/stitching/incremental.ts +5 -3
package/CHANGELOG.md CHANGED
@@ -16,6 +16,157 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.13.0] — 2026-05-29
20
+
21
+ ### Added — Layer-2 components absorbed into `<Camera>` (opt-out)
22
+
23
+ The flagship `<Camera>` now ships built-in defaults for every UX
24
+ chrome piece previously exposed only as a Layer-2 component. Hosts
25
+ adopting `<Camera>` directly get a complete capture surface — flash
26
+ button, pan-speed pill, drift-marker guide, header chrome,
27
+ capture-history strip, and post-stitch preview — without having to
28
+ import and wire each piece by hand.
29
+
30
+ All built-ins use the opt-out pattern: enabled by default, disabled
31
+ by setting the corresponding boolean to `false` or by omitting the
32
+ corresponding payload prop. Hosts that want their own chrome can
33
+ opt out per piece and layer custom UI on top of `<Camera>` (the
34
+ Layer-2 components remain exported and are unchanged).
35
+
36
+ #### Flash control
37
+
38
+ - `flash?: 'on' | 'off'` — controlled torch state. Omit to let
39
+ `<Camera>` own it internally.
40
+ - `onFlashChange?` — fires on tap (controlled and uncontrolled both).
41
+ - `showFlashButton?: boolean` (default `true`) — built-in flash button
42
+ in the bottom-left slot. AR mode auto-disables (ARKit / ARCore own
43
+ the device's torch; surfaces "Flash unavailable in AR mode" a11y
44
+ label and greyed styling).
45
+
46
+ #### Pan guidance
47
+
48
+ - `panGuide?: boolean` (default `true`) — built-in
49
+ `IncrementalPanGuide` ("keep the arrow on the line" drift marker).
50
+ - `panoramaGuidance?: boolean` (default `true`) — built-in
51
+ `PanoramaGuidance` pan-speed pill.
52
+ - Both are gyroscope-driven and only subscribe to the sensor while
53
+ recording — no idle cost.
54
+
55
+ #### Header
56
+
57
+ - `headerTitle?: string` — when set, renders a built-in
58
+ `CaptureHeader` at the top of the screen. The existing settings
59
+ gear is absorbed into the header's right side (no duplicate gear).
60
+ - `onHeaderBack?`, `headerBackLabel?`, `headerGuidance?`,
61
+ `headerColors?` — pass-through to `CaptureHeader`.
62
+
63
+ #### Capture history + preview
64
+
65
+ - `thumbnails?: CaptureThumbnailItem[]` — when supplied (even `[]`),
66
+ renders the built-in `CaptureThumbnailStrip` above the bottom
67
+ controls. Hidden during recording so it doesn't overlap the
68
+ panorama band overlay.
69
+ - `thumbnailsMin?`, `thumbnailsMax?` — count-line hints.
70
+ - `onThumbnailPress?` — replaces the strip's built-in
71
+ tap-to-preview modal with a host handler.
72
+ - `capturePreview?` — when set, renders a built-in `CapturePreview`
73
+ modal showing the supplied image. Use for post-stitch
74
+ confirmation; the host clears the prop on dismiss via
75
+ `onCapturePreviewClose`.
76
+ - `capturePreviewActions?` — pass-through action buttons for the
77
+ preview modal.
78
+
79
+ ### Migration
80
+
81
+ - Hosts that were importing Layer-2 components (`CaptureHeader`,
82
+ `CaptureControlsBar`, `IncrementalPanGuide`, `PanoramaGuidance`,
83
+ `CaptureThumbnailStrip`, `CapturePreview`) directly can now drop
84
+ those imports and use the corresponding `<Camera>` props.
85
+ - The Layer-2 components remain exported and unchanged in v0.13 for
86
+ backward compatibility. Deprecation of those exports is targeted
87
+ for v0.14.
88
+ - No behaviour change for hosts that already use `<Camera>` and
89
+ don't supply any of the new props — every new built-in defaults
90
+ to the previous (omitted) UX, except the flash button which
91
+ appears in the now-occupied bottom-left slot. Hosts that previously
92
+ rendered chrome in that slot above `<Camera>` can pass
93
+ `showFlashButton={false}`.
94
+
95
+ ## [0.12.0] — 2026-05-28
96
+
97
+ ### Added — Orientation-aware `<Camera>` (R2-lite)
98
+
99
+ `<Camera>` now works correctly under both portrait-locked and
100
+ non-locked iOS hosts. Pre-v0.12 the component assumed the host
101
+ had restricted `UISupportedInterfaceOrientations` to Portrait;
102
+ removing that restriction broke control layout, camera-preview
103
+ rotation across modal close, and panorama capture mode selection.
104
+
105
+ Five coupled changes:
106
+
107
+ 1. **`useOrientationDrift` hook + `OrientationDriftModal`**
108
+ (PR-1). Snapshots device orientation at capture start and
109
+ latches a `drifted: true` flag if the user rotates mid-
110
+ capture. The incremental engine doesn't support cross-
111
+ orientation captures (per the engine spec at
112
+ `incremental.ts:373-403`), so `<Camera>` auto-cancels via
113
+ `incremental.cancel()` and shows the modal to explain.
114
+
115
+ 2. **New `onCaptureAbandoned` prop** on `<Camera>`. Fires when
116
+ the SDK auto-cancels an in-flight capture. Currently the only
117
+ reason is `'orientation-drift'`; the union signature keeps the
118
+ prop stable for future reasons (low memory, etc.).
119
+
120
+ 3. **4-way home-indicator-edge anchor** for the bottom-controls
121
+ row (Layer A). Combines `useWindowDimensions()` and
122
+ `useDeviceOrientation()` to compute the JS edge that
123
+ corresponds to the device's home-indicator side, then anchors
124
+ shutter / lens / AR toggle there. Matches iOS Camera's
125
+ behaviour: shutter stays within thumb reach regardless of tilt.
126
+
127
+ 4. **AR `takePhoto` orientation parameter** (Fix #2). Pre-v0.12
128
+ `RNSARSession.takePhoto` hardcoded `.right` (90° CW) to
129
+ rotate ARKit's sensor-native landscape buffer to portrait,
130
+ assuming portrait hold. Now switches on the device
131
+ orientation passed from `useDeviceOrientation()` so landscape
132
+ captures produce correctly-oriented photos.
133
+
134
+ 5. **Modal `supportedOrientations={[all 4]}`** on
135
+ `OrientationDriftModal` and `PanoramaSettingsModal`. RN's iOS
136
+ `Modal` defaults to portrait-only, which force-rotates the
137
+ window scene when opened under a non-locked host — leaving
138
+ the underlying `<Camera>`'s ARSession with stale orientation
139
+ state on dismiss (preview rendered sideways). Declaring all
140
+ four orientations keeps the window aligned through the modal
141
+ cycle.
142
+
143
+ ### Added — Comment cleanup across native + JS surfaces
144
+
145
+ Stale "portrait-locked host" comments in
146
+ `useDeviceOrientation.ts`, `incremental.ts`, `StitcherFrame.ts`,
147
+ `OpenCVIncrementalStitcher.{h,mm}`, and
148
+ `IncrementalFirstwinsEngine.kt` rewritten to acknowledge both
149
+ host configurations. Pose-derived orientation detection remains
150
+ the single source of truth — that didn't change; the rationale
151
+ just got more accurate.
152
+
153
+ ### Known follow-ups (deferred to v0.12.1 or v0.13)
154
+
155
+ - Portrait + non-AR stitching can regress under fast horizontal
156
+ pans — likely drift detection over-firing on lateral
157
+ acceleration. Needs debounce or motion-aware threshold.
158
+ - Component-render tests (`<OrientationDriftModal>`,
159
+ `<PanoramaBandOverlay>` per-orientation, `<ViewportCropOverlay>`
160
+ per-orientation, `<Camera>` composition) need
161
+ `@testing-library/react-native` + a jest preset flip. Tracked
162
+ for v0.12.1.
163
+ - Portrait-upside-down landscape detection on non-locked hosts —
164
+ the JS dims signal is ambiguous between locked-portrait + flipped
165
+ device and non-locked + screen-flipped-180°. Needs a separate
166
+ signal.
167
+ - Slot/hybrid API on `<Camera>` to absorb `CaptureControlsBar`,
168
+ `IncrementalPanGuide`, `PanoramaGuidance`, etc. — v0.13.
169
+
19
170
  ## [0.11.1] — 2026-05-28
20
171
 
21
172
  ### Fixed — AR-mode composed worklets silently throw
package/README.md CHANGED
@@ -140,6 +140,34 @@ See `src/camera/Camera.tsx` for the full TSDoc. Highlights:
140
140
  | `onFramesDropped(info)` | cv::Stitcher's confidence retry loop dropped one or more input frames. |
141
141
  | `onError(err)` | Classified error. `err.code` from a known taxonomy (`STITCH_NEED_MORE_IMGS`, `STITCH_HOMOGRAPHY_FAIL`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_OOM`, `CAMERA_PERMISSION_DENIED`, etc.). |
142
142
 
143
+ ## Orientation support
144
+
145
+ `<Camera>` works in any device orientation regardless of host
146
+ configuration. No host setup required — the SDK adapts at runtime.
147
+
148
+ **Portrait-locked host** (Info.plist `UISupportedInterfaceOrientations`
149
+ restricted to Portrait — recommended for kiosks / single-task apps):
150
+ the screen stays portrait; the SDK uses sensor-derived orientation
151
+ for capture-mode selection and overlay layout. This is the simpler
152
+ configuration and the historical default.
153
+
154
+ **Non-locked host** (Info.plist supports all 4 orientations — recommended
155
+ for apps with other landscape-friendly screens): the screen rotates
156
+ with the device. `<Camera>`'s controls (shutter, lens chip, AR toggle)
157
+ anchor to the home-indicator edge so they stay within thumb reach
158
+ regardless of tilt — matching iOS Camera's behaviour. The
159
+ orientation-aware logic combines `useWindowDimensions()` (JS-layout)
160
+ with `useDeviceOrientation()` (sensor) to compute the correct anchor.
161
+
162
+ **Mid-capture rotation safety** — the incremental engine doesn't
163
+ support cross-orientation captures (a portrait capture's keyframes
164
+ can't be mixed with landscape-pan frames). If the user rotates
165
+ mid-capture, `<Camera>` auto-abandons via `incremental.cancel()`,
166
+ fires `onCaptureAbandoned('orientation-drift')` if the host wired
167
+ the callback, and shows the `OrientationDriftModal` to explain why.
168
+ Host opt-in via the `onCaptureAbandoned` prop — the default UX is
169
+ the modal alone.
170
+
143
171
  ## Lens ↔ AR interaction
144
172
 
145
173
  | Action | `arPreference` | `lens` | UI |
@@ -38,8 +38,9 @@ import kotlin.math.sqrt
38
38
  * V12.4 — central 70% (pan) × 85% (perpendicular) post-warp crop
39
39
  * V12.6 — orientation detection from R_panToCam at first frame
40
40
  * (NOT from JS-passed frameRotationDegrees, which is wrong
41
- * under iOS interface-orientation lockAndroid equivalent
42
- * is screen-orientation lock; same fix applies)
41
+ * under portrait-locked hostspose-derived detection
42
+ * works regardless of host orientation config, so it's
43
+ * the single source of truth)
43
44
  * V12.7 — rectilinear path: skip cylindrical warp entirely. First
44
45
  * frame pasted raw onto canvas; subsequent frames contribute
45
46
  * a narrow central strip placed by pose-delta around the
@@ -60,6 +60,16 @@ export interface ARCameraViewHandle {
60
60
  */
61
61
  takePhoto: (options?: {
62
62
  quality?: number;
63
+ /**
64
+ * v0.12.0 — device orientation at capture time, used to bake
65
+ * correct rotation into the saved JPEG. Pass the value from
66
+ * `useDeviceOrientation()`. Defaults to `'portrait'` on the
67
+ * native side if omitted (preserves pre-v0.12 behavior).
68
+ * Without this, AR-mode photos taken in landscape come out
69
+ * sideways because the native side previously hardcoded the
70
+ * rotate-to-portrait assumption.
71
+ */
72
+ orientation?: 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
63
73
  }) => Promise<{
64
74
  path: string;
65
75
  width: number;
@@ -90,6 +90,7 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
90
90
  return native.takePhoto({
91
91
  path: '',
92
92
  quality: options.quality ?? 90,
93
+ orientation: options.orientation ?? 'portrait',
93
94
  });
94
95
  },
95
96
  startRecording: (options) => {
@@ -41,6 +41,9 @@
41
41
  import React from 'react';
42
42
  import { type StyleProp, type ViewStyle } from 'react-native';
43
43
  import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
44
+ import { type CaptureHeaderProps } from './CaptureHeader';
45
+ import { type CapturePreviewAction } from './CapturePreview';
46
+ import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
44
47
  export type CaptureSource = 'ar' | 'non-ar';
45
48
  export type CameraLens = '1x' | '0.5x';
46
49
  export type StitchMode = 'auto' | 'panorama' | 'scans';
@@ -191,6 +194,194 @@ export interface CameraProps {
191
194
  onLensChange?: (lens: CameraLens) => void;
192
195
  onFramesDropped?: (info: FramesDroppedInfo) => void;
193
196
  onError?: (err: CameraError) => void;
197
+ /**
198
+ * v0.12.0 — fires when the SDK auto-abandons an in-progress
199
+ * capture without producing output. `reason` is a string union
200
+ * so future reasons (network loss, low memory, etc.) can be added
201
+ * without breaking the callback signature.
202
+ *
203
+ * Currently the only reason in v0.12 is `'orientation-drift'`:
204
+ * the user rotated the device between Mode A (landscape + vertical
205
+ * pan) and Mode B (portrait + horizontal pan) mid-capture. The
206
+ * engine docstring at `incremental.ts:373-403` is explicit that
207
+ * cross-mode capture is "best-effort, not supported," so the SDK
208
+ * decisively cancels the capture (`incremental.cancel()`) and
209
+ * surfaces `OrientationDriftModal` to explain what happened.
210
+ *
211
+ * Hosts use this callback to clean up their own state (e.g., reset
212
+ * a wizard step, log telemetry, surface their own retry UX in
213
+ * addition to the SDK's built-in modal). No `onCapture` will fire
214
+ * for an abandoned capture.
215
+ */
216
+ onCaptureAbandoned?: (reason: 'orientation-drift') => void;
217
+ /**
218
+ * v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
219
+ *
220
+ * - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
221
+ * state internally. Tapping the built-in flash button toggles
222
+ * it on/off. `onFlashChange` (if supplied) fires for telemetry.
223
+ * - **Controlled** (supply `flash`): the parent owns the state.
224
+ * The built-in button still renders and fires `onFlashChange`
225
+ * on press, but it's a no-op unless the parent updates `flash`
226
+ * in response.
227
+ *
228
+ * Both shapes coexist with the v0.13 "flash button is on by default"
229
+ * built-in (see the bottom-left bar slot in the JSX). Hosts that
230
+ * want their own flash chrome can opt out via `showFlashButton={false}`
231
+ * and drive the underlying torch by controlling `flash` directly.
232
+ *
233
+ * ## AR-mode behaviour
234
+ *
235
+ * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
236
+ * ARKit / ARCore own the `AVCaptureDevice` and don't expose the
237
+ * torch through vision-camera's pipeline. The built-in flash
238
+ * button renders as visibly disabled (a11y label "Flash unavailable
239
+ * in AR mode") and `flash` is forced to `'off'` regardless of
240
+ * controlled/uncontrolled state. Hosts that need flash should
241
+ * toggle to non-AR before enabling.
242
+ */
243
+ flash?: 'on' | 'off';
244
+ /**
245
+ * v0.13.0 — fires when the user taps the built-in flash button.
246
+ * In uncontrolled mode, the internal state has already flipped
247
+ * (single render delay). In controlled mode, the parent must
248
+ * update the `flash` prop in response or the visual toggle is
249
+ * a no-op. Useful in either mode for telemetry.
250
+ */
251
+ onFlashChange?: (next: 'on' | 'off') => void;
252
+ /**
253
+ * v0.13.0 — show the built-in flash button in the bottom-left
254
+ * slot. Defaults to `true`. Hosts that render their own flash
255
+ * chrome (and drive the underlying torch via the controlled
256
+ * `flash` prop) can opt out by setting this to `false`.
257
+ */
258
+ showFlashButton?: boolean;
259
+ /**
260
+ * v0.13.0 — show the built-in IncrementalPanGuide ("keep the
261
+ * arrow on the line" drift marker) while recording. Defaults
262
+ * to `true`. The guide is gyroscope-driven and only active
263
+ * during the recording phase (no idle sensor cost). Hosts that
264
+ * want their own pan-guide chrome can opt out via `false`.
265
+ */
266
+ panGuide?: boolean;
267
+ /**
268
+ * v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
269
+ * ("Pan slowly" / "Slow down" / "Too fast") while recording.
270
+ * Defaults to `true`. Gyroscope-driven, only active during
271
+ * recording. Hosts that want their own speed chrome can opt
272
+ * out via `false`.
273
+ */
274
+ panoramaGuidance?: boolean;
275
+ /**
276
+ * v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
277
+ * renders a top-of-screen header showing this title (centred)
278
+ * with an optional back affordance + guidance subtitle + the
279
+ * existing settings gear absorbed into the header's right side.
280
+ *
281
+ * When `headerTitle` is undefined the header is not rendered
282
+ * (matches pre-v0.13 behaviour: top of preview is bare except
283
+ * for the standalone settings gear gated on `showSettingsButton`).
284
+ *
285
+ * Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
286
+ * and `headerColors` to customise the rest of the header. Hosts
287
+ * that need richer header chrome can omit `headerTitle` and
288
+ * compose their own `<CaptureHeader>` above `<Camera>`.
289
+ */
290
+ headerTitle?: string;
291
+ /**
292
+ * v0.13.0 — header back-button callback. When supplied (and
293
+ * `headerTitle` is set), the header renders a back affordance
294
+ * on the left. Omitted ⇒ no back button (the title stays
295
+ * centred).
296
+ */
297
+ onHeaderBack?: () => void;
298
+ /**
299
+ * v0.13.0 — header back-button label. Defaults to "‹ Back".
300
+ * No effect unless `headerTitle` and `onHeaderBack` are both set.
301
+ */
302
+ headerBackLabel?: string;
303
+ /**
304
+ * v0.13.0 — optional second-line subtitle shown below the
305
+ * header title. E.g. "Photograph the promotional cola end cap."
306
+ * Renders nothing when undefined. No effect unless `headerTitle`
307
+ * is set.
308
+ */
309
+ headerGuidance?: string;
310
+ /**
311
+ * v0.13.0 — colour overrides for the built-in header. Defaults
312
+ * are white-on-black to stay legible over the camera preview.
313
+ * No effect unless `headerTitle` is set.
314
+ */
315
+ headerColors?: CaptureHeaderProps['colors'];
316
+ /**
317
+ * v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
318
+ * built-in `CaptureThumbnailStrip` above the bottom controls
319
+ * showing the host's capture history. Each item is a plain
320
+ * `{ id, uri, width?, height? }` object; the strip handles
321
+ * aspect-ratio rendering, tap-to-preview, and the count line.
322
+ *
323
+ * Omit (`undefined`) to skip the strip entirely. Hosts using
324
+ * the strip independently (e.g. on a non-camera screen) can keep
325
+ * importing `CaptureThumbnailStrip` directly from the library —
326
+ * the prop here is the convenience wiring for in-`<Camera>` use.
327
+ *
328
+ * Captures emitted by `<Camera>`'s `onCapture` are NOT added to
329
+ * this array automatically — the host owns the canonical list
330
+ * (typically persisted to its own DB) and updates the prop in
331
+ * response. This matches the SDK's "Camera owns runtime state,
332
+ * host persists" pattern.
333
+ */
334
+ thumbnails?: CaptureThumbnailItem[];
335
+ /**
336
+ * v0.13.0 — minimum-photos hint for the count line. Renders
337
+ * "n / minPhotos min" with the success colour when reached,
338
+ * warning colour otherwise.
339
+ */
340
+ thumbnailsMin?: number;
341
+ /**
342
+ * v0.13.0 — maximum-photos hint for the count line. Renders
343
+ * "· maxPhotos max" suffix. No enforcement — the host decides
344
+ * what to do at the cap.
345
+ */
346
+ thumbnailsMax?: number;
347
+ /**
348
+ * v0.13.0 — tap handler for thumbnails. When set, replaces the
349
+ * strip's built-in tap-to-preview modal; the host shows its own
350
+ * preview UI (e.g. with delete / recapture buttons gated on
351
+ * sync state). Omit to use the built-in preview.
352
+ */
353
+ onThumbnailPress?: (item: CaptureThumbnailItem) => void;
354
+ /**
355
+ * v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
356
+ * modal as `visible`. Use this for post-stitch confirmation:
357
+ * after `onCapture` emits, the host stores the result and sets
358
+ * `capturePreview` to the new image, with `capturePreviewActions`
359
+ * = `[Discard, Save]` (or similar). Setting `undefined` hides
360
+ * the modal.
361
+ *
362
+ * Hosts using the modal for thumbnail tap-to-preview can leave
363
+ * this undefined and let the built-in strip's preview handle
364
+ * that case.
365
+ */
366
+ capturePreview?: {
367
+ imageUri: string;
368
+ imageWidth?: number;
369
+ imageHeight?: number;
370
+ title?: string;
371
+ };
372
+ /**
373
+ * v0.13.0 — action buttons rendered along the bottom of the
374
+ * `CapturePreview` modal. Empty array (or undefined) renders
375
+ * no buttons, only the close affordance.
376
+ */
377
+ capturePreviewActions?: CapturePreviewAction[];
378
+ /**
379
+ * v0.13.0 — fires when the user dismisses the `capturePreview`
380
+ * modal (tap close, backdrop tap, hardware back on Android).
381
+ * The host is expected to clear the `capturePreview` prop in
382
+ * response.
383
+ */
384
+ onCapturePreviewClose?: () => void;
194
385
  /**
195
386
  * Optional host-supplied vision-camera frame processor.
196
387
  *