/**
* Camera — the public, props-based camera component for the
* `react-native-image-stitcher` library (publication target per the
* 2026-05-15 design doc).
*
* One component, both modes:
* - **Tap shutter** → single photo via vision-camera's takePhoto
* (non-AR) or ARFrame.capturedImage (AR).
* - **Hold shutter** → panorama capture; pan-and-release produces
* a stitched panorama JPEG via the incremental stitcher.
*
* One component, both capture sources:
* - **AR mode** (ARKit / ARCore) — used for pose-aware stitching
* when the device supports it.
* - **Non-AR mode** (vision-camera + IMU) — fallback path,
* forced when the 0.5× ultra-wide lens is selected (AR sessions
* are tied to a single physical lens; can't switch mid-session).
*
* The Camera component owns its runtime state (arPreference, lens,
* settings). Parent props are read as INITIAL VALUES at mount; the
* parent listens for state changes via the callback props. This
* "uncontrolled" model matches React's `` convention and
* matches the design doc's intent (NF — component owns runtime state,
* parent persists via callbacks if desired).
*
* Scope note (step 2 of the SDK extract plan):
* - Props-driven API for both photo + panorama modes — DONE here.
* - Lens chip + AR toggle UI (U1) — DONE here.
* - `showSettingsButton` gates the existing PanoramaSettingsModal — DONE.
* - Imperative ref methods (`takePhoto()`, `startPanorama()`,
* `stopPanorama()`) — deferred; the built-in shutter button is the
* primary affordance for v0.1.0.
* - Forward-looking props (`defaultCompositingResolMP`,
* `defaultRegistrationResolMP`, `defaultSeamEstimationResolMP`)
* are accepted but currently no-ops — those fields don't exist on
* PanoramaSettings yet. They're declared so the public API is
* stable before they wire through; the wiring is a follow-up.
*
* See: docs/site-content/design/2026-05-15-react-native-image-stitcher-publication.md
*/
import React from 'react';
import { type StyleProp, type ViewStyle } from 'react-native';
import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
import type { AROverlay } from '../stitching/AROverlay';
import type { AROverlayMethods } from './arOverlayController';
import { type CaptureHeaderProps } from './CaptureHeader';
import { type CapturePreviewAction } from './CapturePreview';
import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
import { type CaptureStatusPhase } from './CaptureStatusOverlay';
import { type PanoramaPropOverrides } from './buildPanoramaInitialSettings';
import { type DeviceOrientation } from './useDeviceOrientation';
import { type PanMode } from './panModeGate';
import type { LateralMotionModel } from './usePanMotion';
import { type GuidanceCopy } from './cameraGuidanceCopy';
import { type CaptureWarning } from './captureWarnings';
export type CaptureSource = 'ar' | 'non-ar';
/**
* v0.13.2 — which capture sources the host ALLOWS. A constraint on top
* of `defaultCaptureSource` (which picks the initial source within this
* constraint):
* 'both' — AR and non-AR both available; AR toggle is shown.
* 'ar' — AR only; AR toggle hidden (nothing to switch to), and the
* 0.5× lens chooser is hidden (ARKit/ARCore don't expose the
* ultra-wide).
* 'non-ar' — non-AR only; AR toggle hidden.
*/
export type CaptureSourcesMode = 'ar' | 'non-ar' | 'both';
export type CameraLens = '1x' | '0.5x';
export type StitchMode = 'auto' | 'panorama' | 'scans';
export type Blender = 'multiband' | 'feather';
export type SeamFinder = 'graphcut' | 'skip';
export type Warper = 'plane' | 'cylindrical' | 'spherical';
/**
* Result emitted via `onCapture`. Discriminated union keyed FIRST on
* `ok` (success vs. failure) and then on `type` (photo vs. panorama), so a
* host handles EVERY capture outcome — success, degraded success, and
* failure — through this one callback.
*
* ## v0.16 — unified success/failure + warnings (BREAKING)
*
* Previously `onCapture` fired only on success and carried no `ok` field;
* failures went *solely* to `onError`. Hosts therefore had no single place
* to learn whether a capture succeeded, and no programmatic signal that a
* stitch was *degraded* (e.g. most frames dropped). Now:
*
* - `onCapture` ALWAYS fires once per capture attempt, with `ok:true`
* (output present) or `ok:false` (carrying the `CameraError`).
* - both success and failure carry `warnings: CaptureWarning[]` — non-fatal
* quality signals (e.g. `LOW_FRAME_UTILIZATION` when <70 % of captured
* frames survived, `LATERAL_DRIFT_FINALIZE` when item-6 stopped early).
* - `onError` STILL fires on failure too (an unchanged mirror), so existing
* error handling keeps working.
*
* Migration: gate on `ok` before reading `uri`/`width`/`height` —
* `if (!result.ok) { handle(result.error); return; }`.
*
* Identifier `CameraCaptureResult` (vs. the SDK's existing `CaptureResult`
* from `../types`) is intentional — the existing CaptureResult shape has
* SDK-specific fields that don't belong in the public RN library's surface.
*/
export type CameraCaptureResult = {
ok: true;
type: 'photo';
uri: string;
width: number;
height: number;
/**
* iOS `captureDepthData` (NON-AR captures only) — path of the
* `.depth.bin` depth sidecar saved next to the photo
* (float32 metres row-major + JSON header with dims/intrinsics;
* format spec in `website/docs/photo-depth.md`). Absent on
* Android, in AR capture, on depth-less devices/formats, and
* whenever the opt-in is off.
*/
depthPath?: string;
/**
* WHY `depthPath` is absent although `captureDepthData` was
* requested (iOS non-AR): the extractor's reason slug
* (`no-depth-aux` = no auxiliary depth in the capture — typically
* a non-depth-capable mounted device; `native-module-missing` =
* JS newer than the installed binary). Diagnostic only.
*/
depthUnavailableReason?: string;
/** Non-fatal quality signals (empty when none). */
warnings: CaptureWarning[];
} | {
ok: true;
type: 'panorama';
uri: string;
width: number;
height: number;
framesRequested: number;
framesIncluded: number;
framesDropped: number;
finalConfidenceThresh: number;
durationMs: number;
/**
* 2026-05-22 (audit F2g) — which cv::Stitcher pipeline the
* batch finalize ran (after auto-resolution if applicable).
* Useful for displaying a "Stitched as: scans" pill on the
* output preview. Undefined when the engine wasn't
* batch-keyframe (hybrid / slit-scan don't go through
* cv::Stitcher at finalize).
*/
stitchModeResolved?: 'panorama' | 'scans';
/**
* 2026-06-15 (DEV) — gyro rotation magnitude of the capture, in radians.
* Shown on the dev preview so the panorama-vs-SCANS rotation threshold can
* be tuned. `0` = no pose-derived rotation signal (non-AR with no poses).
*/
rRadians?: number;
/**
* 2026-06-16 (DEV) — translation magnitude (m) + auto decision ratio
* (`>=0.55` → SCANS) that drove panorama-vs-SCANS. Shown on the dev
* readout alongside `rRadians` to tune the threshold from real captures.
*/
tMeters?: number;
decisionRatio?: number;
/**
* 2026-06-14 (DEV overlay) — semicolon-separated `key=value` trace of the
* stitcher's runtime choices (pipe/warp/route/seam/blend) for this
* output. Shown on the preview in __DEV__. iOS only for now.
*/
debugSummary?: string;
/**
* 2026-06-15 (iOS) — keyframe JPEG paths used for this stitch, so the
* preview can re-stitch them on demand via `refinePanorama` (the
* high-level tab). iOS only; undefined elsewhere.
*/
keyframePaths?: string[];
/**
* 2026-06-15 (iOS) — orientation this stitch baked in. The on-demand
* high-level re-stitch passes it back so it matches the manual output's
* rotation (not the raw sensor landscape). iOS only.
*/
captureOrientation?: string;
/** Non-fatal quality signals (empty when none). */
warnings: CaptureWarning[];
} | {
ok: false;
/** Which capture path failed. */
type: 'photo' | 'panorama';
/** The classified failure (same object handed to `onError`). */
error: CameraError;
/** Any warnings gathered before the failure (usually empty). */
warnings: CaptureWarning[];
};
/**
* The success-panorama variant of {@link CameraCaptureResult} — the exact
* shape stashed for the crop editor and re-emitted (with adjusted dims) once
* the user crops. Narrowed so the crop-confirm spread keeps `uri`/`width`/
* `height`/`ok` without a cast.
*/
export type PanoramaCaptureResult = Extract;
/**
* Errors surfaced via `onError`. Classified codes so consumers can
* branch on the kind of failure (toast vs retry vs report).
*/
export type CameraErrorCode = 'CAMERA_PERMISSION_DENIED' | 'CAMERA_DEVICE_UNAVAILABLE' | 'PHOTO_CAPTURE_FAILED' | 'PANORAMA_START_FAILED' | 'PANORAMA_FINALIZE_FAILED' | 'STITCH_NEED_MORE_IMGS' | 'STITCH_HOMOGRAPHY_FAIL' | 'STITCH_CAMERA_PARAMS_FAIL'
/**
* v0.16 — the native post-stitch validator rejected the output: the
* panorama came out disjoint / fragmented / wildly mis-proportioned
* (frames didn't connect into one coherent image). Recoverable by
* re-capturing, so it carries "try again" copy.
*/
| 'STITCH_LOW_QUALITY' | 'STITCH_OOM' | 'OUTPUT_WRITE_FAILED'
/**
* Vision-camera surfaced a runtime error that isn't a known
* transient lifecycle event (those are swallowed inside the SDK's
* ``). Examples that DO reach the host as this code:
* `format/invalid-format`, `capture/recording-canceled`,
* `device/microphone-permission-denied`, ... The full error
* object is on `.cause` for inspection.
*/
| 'VISION_CAMERA_RUNTIME' | 'UNKNOWN';
export declare class CameraError extends Error {
readonly code: CameraErrorCode;
readonly cause?: unknown;
constructor(code: CameraErrorCode, message: string, cause?: unknown);
}
/**
* Frames-dropped info delivered via `onFramesDropped`. Fires once
* per panorama capture if the native stitch retry (the flattened
* 4-rung mode/threshold ladder since 2026-08-17) promoted a result
* that dropped one or more input frames.
*/
export interface FramesDroppedInfo {
requested: number;
included: number;
}
/**
* Camera component props. See the design doc's "Component API"
* section for the full rationale per field.
*/
export interface CameraProps {
/**
* Initial capture source. Default `'non-ar'`.
*
* `'ar'` feeds the engine natively from the ARKit/ARCore session, so
* it does NOT depend on the vision-camera frame-processor chain that
* non-AR capture requires (see the "Frame processors" section of the
* host-integration docs). If your fleet is AR-capable, opting in with
* `defaultCaptureSource="ar"` — or locking it with `captureSources="ar"`,
* which also hides the runtime AR toggle — avoids that whole class of
* build-time integration failure.
*
* Caveats when choosing AR: on Android, devices without "Google Play
* Services for AR" installed are prompted to install it, and a declined
* install currently leaves the AR preview blank (no automatic downgrade
* — fixed in a later release); AR tap-photos also come from the AR video
* stream rather than the full-resolution still pipeline, the flash is
* unavailable, and the iOS depth sidecar is not produced.
*
* Devices without AR support (and the 0.5× lens) always resolve to
* non-AR regardless of this value.
*/
defaultCaptureSource?: CaptureSource;
defaultLens?: CameraLens;
defaultStitchMode?: StitchMode;
defaultBlender?: Blender;
defaultWarper?: Warper;
defaultFlowNoveltyPercentile?: number;
defaultFlowEvalEveryNFrames?: number;
defaultFlowMaxTranslationCm?: number;
defaultKeyframeMaxCount?: number;
defaultKeyframeOverlapThreshold?: number;
/** Time-budget force-accept (ms) for the keyframe gate — accept a
* keyframe at least this often during a pan even if novelty is low,
* so slow / static pans don't leave temporal gaps. `0` disables it.
* Default 2000 (2 s). Applies to both AR and non-AR captures. */
defaultMaxKeyframeIntervalMs?: number;
/** Forward-looking — wires through to cv::Stitcher's compositingResol
* once PanoramaSettings exposes the field (currently a no-op). */
defaultCompositingResolMP?: number;
/** Forward-looking — see above. */
defaultRegistrationResolMP?: number;
/** Forward-looking — see above. */
defaultSeamEstimationResolMP?: number;
/**
* v0.16 — the stitch RECIPE as a JSON object (`stitchMode` / `warperType` /
* `blenderType` / `enableMaxInscribedRectCrop` / `debugPack`). Partial; wins
* over the flat `default*` props. v0.24 — the speed levers moved to {@link
* perf}. */
stitcher?: PanoramaPropOverrides['stitcher'];
/**
* v0.16 — the keyframe GATE as a JSON object (`mode` / `maxKeyframes` /
* `overlapThreshold` / `maxKeyframeIntervalMs` / `flow`). Partial; `flow` is
* deep-merged. v0.24 — the anti-blur controls moved to {@link blur}. */
frameSelection?: PanoramaPropOverrides['frameSelection'];
/**
* v0.24 — **anti-blur** controls in one group: `sharpnessWindow` (pick-
* sharpest-of-K) + exposure cap + motion gate + sharpness floor + hi-fps
* format. Deep-merged over the SDK defaults (all ON). Set a knob to 0 /
* false (or `sharpnessWindow: 1`) to disable it. */
blur?: PanoramaPropOverrides['blur'];
/**
* v0.24 — **perf** (stitch-speed) levers in one group: `seamFinderType`
* (default 'voronoi'), `rangeMatcherWidth` (3), `numThreads` (0 = multi),
* `adaptiveStitchMode` ('measured') + its `adaptiveMinOutputMP` /
* `adaptiveSlowStitchMsPerFrame`. Wins over `stitcher`. */
perf?: PanoramaPropOverrides['perf'];
/**
* Crop strategy for the stitched panorama. `false` (default) keeps the
* bounding-rect of non-black pixels, which preserves all stitched
* content but may leave black corners. `true` crops to the maximum
* axis-aligned rectangle inscribed in the coverage mask — clean edges,
* no black corners (slightly more CPU at finalize) — but it can shrink
* the output substantially on lopsided / ultra-wide masks, which is why
* it's opt-in.
*
* Implemented as a start-time stitcher config (like the other
* stitcher settings), so this value is read once at mount to seed the
* initial setting; the in-app settings modal can override it at
* runtime. It changes image geometry (the crop), not encoding.
*
* Since the default is `false`, only pass this prop to opt in:
* @example
* // Crop to a clean inscribed rectangle (no black corners):
*
*/
maxInscribedRectCrop?: boolean;
/**
* Default `true`. Set `false` to disable single-TAP photo capture
* entirely (`handleTap` no-ops, same as `shutterDisabled`) and hide the
* native-0.5× external-camera fallback (`offerNativeUW`) — that
* fallback captures ONE still via the OS camera, the same shape of
* action this flag disables.
*
* PANO-ONLY RECIPE: `enablePhotoMode={false}` with `enablePanoramaMode`
* left at its default `true` is already a pano-only `` — tap is
* disabled, hold-to-pan still fires a capture. No separate flag needed.
* Lens switching (the 0.5×/1× chip) is unaffected either way — it
* selects which device the eventual hold-to-pan uses, it does not
* itself capture.
*/
enablePhotoMode?: boolean;
enablePanoramaMode?: boolean;
/**
* Hide the built-in shutter + AR-toggle so a HOST can render its own capture
* controls and drive capture through the imperative handle
* ({@link CameraHandle.takePhoto} / {@link CameraHandle.startPanorama} /
* {@link CameraHandle.stopPanorama}). The lens chip is KEPT — it is a lens
* selector, not a capture control, and the whole point is that the library
* still owns lens selection. Default `false` (built-in shutter shown, every
* existing consumer unchanged).
*/
hideBuiltInShutter?: boolean;
/**
* When a device offers only ONE usable lens (no in-app ultra-wide and no
* native-0.5× fallback on offer), render NOTHING instead of a static "1×"
* label — there is nothing to switch, so the chip is noise. Default `false`
* keeps the "1×" label for back-compat.
*/
hideLensChipWhenSingle?: boolean;
/**
* Lift the bottom control cluster (lens chip + built-in shutter, if shown) by
* this many px, so a host chrome docked below the preview (e.g. a mode
* switcher) doesn't overlap it. Default `0`. Layout-only; no behaviour change.
*/
bottomBarOffset?: number;
showSettingsButton?: boolean;
/**
* v0.13.2 — which capture sources the host allows (default `'both'`).
* Constrains both the runtime AR toggle and `defaultCaptureSource`:
* - `'both'` : AR + non-AR; the AR toggle is shown so the user can
* switch at runtime.
* - `'ar'` : AR only. AR toggle hidden (nothing to toggle); the
* 0.5× lens chooser is also hidden (ARKit/ARCore can't use the
* ultra-wide), so the camera stays on the AR-capable 1× lens.
* - `'non-ar'`: non-AR only. AR toggle hidden.
* When set to a single source, that source wins regardless of
* `defaultCaptureSource`.
*/
captureSources?: CaptureSourcesMode;
style?: StyleProp;
/**
* Which stitcher engine to drive. Only `'batch-keyframe'` is
* supported (and the default): it collects accepted keyframe JPEGs
* during the hold-pan-release capture and runs the stitch once at
* finalize. The live engines (hybrid / slit-scan / firstwins) were
* archived in the batch-keyframe cleanup — see `archive/`.
*/
engine?: 'batch-keyframe';
/**
* Optional destination directory for captures. When set, the lib
* lands tap-photos at `${outputDir}/photo-${ts}.jpg` and panoramas
* at `${outputDir}/panorama-${ts}.jpg` and the returned uri points
* at the persisted file (vs. vision-camera's tmp dir, which is
* what you get when this prop is omitted).
*
* The host is solely responsible for:
* - Choosing a writable directory (the lib does NOT pick this for
* you on either platform — particularly relevant on Android,
* where scoped-storage rules differ between app-private storage
* and user-visible Documents/Pictures dirs).
* - Ensuring the directory exists. The lib will create it if it
* doesn't, but only inside paths the OS lets it write to.
* - Making the path user-visible if that matters (`UIFileSharingEnabled`
* on iOS for `FileSystem.documentDirectory`; MediaStore /
* `Documents/...` on Android — see your platform's docs).
*
* On disk failure the capture promise rejects via `onError` with
* `CameraError('OUTPUT_WRITE_FAILED', ...)`. No silent fallback to
* tmp — that hides bugs.
*
* Requires `expo-file-system` (declared as an OPTIONAL peer dep;
* only needed when this prop is set).
*
* Format: bare path or `file://` URI. Both accepted.
*/
outputDir?: string;
/**
* Disable the shutter — taps + holds are ignored and the button paints in
* its disabled visual. The host drives this for capture-gating use cases:
* e.g. a document scanner that only allows capture once the document fills
* the framing guide, or a fixture flow that has reached its max photo count.
* Independent of the SDK's own stitching-in-progress disable. Default
* `false`.
*/
shutterDisabled?: boolean;
onCapture?: (result: CameraCaptureResult) => void;
onCaptureSourceChange?: (source: CaptureSource) => void;
onLensChange?: (lens: CameraLens) => void;
onFramesDropped?: (info: FramesDroppedInfo) => void;
onError?: (err: CameraError) => void;
/**
* v0.12.0 — fires when the SDK auto-abandons an in-progress
* capture without producing output. `reason` is a string union
* so future reasons (network loss, low memory, etc.) can be added
* without breaking the callback signature.
*
* Currently the only reason in v0.12 is `'orientation-drift'`:
* the user rotated the device between Mode A (landscape + vertical
* pan) and Mode B (portrait + horizontal pan) mid-capture. The
* engine docstring at `incremental.ts:373-403` is explicit that
* cross-mode capture is "best-effort, not supported," so the SDK
* decisively cancels the capture (`incremental.cancel()`) and
* surfaces `OrientationDriftModal` to explain what happened.
*
* v0.16 adds `'lateral-drift'`: the user moved the phone perpendicular to
* the pan arrow before enough frames were captured to stitch. Rather than
* finalize into a misleading "need more images" error, the SDK abandons the
* capture and surfaces the `LateralMotionModal` with "follow the arrow"
* copy. (A lateral drift AFTER enough frames still finalizes what was
* captured and fires `onCapture` with a `LATERAL_DRIFT_FINALIZE` warning.)
*
* Hosts use this callback to clean up their own state (e.g., reset
* a wizard step, log telemetry, surface their own retry UX in
* addition to the SDK's built-in modal). No `onCapture` will fire
* for an abandoned capture.
*/
onCaptureAbandoned?: (reason: 'orientation-drift' | 'lateral-drift') => void;
/**
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
*
* - **Uncontrolled** (omit `flash`): `` owns the flash
* state internally. Tapping the built-in flash button toggles
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
* - **Controlled** (supply `flash`): the parent owns the state.
* The built-in button still renders and fires `onFlashChange`
* on press, but it's a no-op unless the parent updates `flash`
* in response.
*
* Both shapes coexist with the v0.13 "flash button is on by default"
* built-in (see the bottom-left bar slot in the JSX). Hosts that
* want their own flash chrome can opt out via `showFlashButton={false}`
* and drive the underlying torch by controlling `flash` directly.
*
* ## AR-mode behaviour
*
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
* torch through vision-camera's pipeline. The built-in flash
* button renders as visibly disabled (a11y label "Flash unavailable
* in AR mode") and `flash` is forced to `'off'` regardless of
* controlled/uncontrolled state. Hosts that need flash should
* toggle to non-AR before enabling.
*/
flash?: 'on' | 'off';
/**
* v0.13.0 — fires when the user taps the built-in flash button.
* In uncontrolled mode, the internal state has already flipped
* (single render delay). In controlled mode, the parent must
* update the `flash` prop in response or the visual toggle is
* a no-op. Useful in either mode for telemetry.
*/
onFlashChange?: (next: 'on' | 'off') => void;
/**
* v0.13.0 — show the built-in flash button in the bottom-left
* slot. Defaults to `true`. Hosts that render their own flash
* chrome (and drive the underlying torch via the controlled
* `flash` prop) can opt out by setting this to `false`.
*/
showFlashButton?: boolean;
/**
* v0.13.0 — built-in CaptureHeader title. When set, ``
* renders a top-of-screen header showing this title (centred)
* with an optional back affordance + guidance subtitle + the
* existing settings gear absorbed into the header's right side.
*
* When `headerTitle` is undefined the header is not rendered
* (matches pre-v0.13 behaviour: top of preview is bare except
* for the standalone settings gear gated on `showSettingsButton`).
*
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
* and `headerColors` to customise the rest of the header. Hosts
* that need richer header chrome can omit `headerTitle` and
* compose their own `` above ``.
*/
headerTitle?: string;
/**
* v0.13.0 — header back-button callback. When supplied (and
* `headerTitle` is set), the header renders a back affordance
* on the left. Omitted ⇒ no back button (the title stays
* centred).
*/
onHeaderBack?: () => void;
/**
* v0.13.0 — header back-button label. Defaults to "‹ Back".
* No effect unless `headerTitle` and `onHeaderBack` are both set.
*/
headerBackLabel?: string;
/**
* v0.13.0 — optional second-line subtitle shown below the
* header title. E.g. "Photograph the promotional cola end cap."
* Renders nothing when undefined. No effect unless `headerTitle`
* is set.
*/
headerGuidance?: string;
/**
* v0.13.0 — colour overrides for the built-in header. Defaults
* are white-on-black to stay legible over the camera preview.
* No effect unless `headerTitle` is set.
*/
headerColors?: CaptureHeaderProps['colors'];
/**
* v0.13.0 — when provided (even as `[]`), `` renders a
* built-in `CaptureThumbnailStrip` above the bottom controls
* showing the host's capture history. Each item is a plain
* `{ id, uri, width?, height? }` object; the strip handles
* aspect-ratio rendering, tap-to-preview, and the count line.
*
* Omit (`undefined`) to skip the strip entirely. Hosts using
* the strip independently (e.g. on a non-camera screen) can keep
* importing `CaptureThumbnailStrip` directly from the library —
* the prop here is the convenience wiring for in-`` use.
*
* Captures emitted by ``'s `onCapture` are NOT added to
* this array automatically — the host owns the canonical list
* (typically persisted to its own DB) and updates the prop in
* response. This matches the SDK's "Camera owns runtime state,
* host persists" pattern.
*/
thumbnails?: CaptureThumbnailItem[];
/**
* v0.13.0 — minimum-photos hint for the count line. Renders
* "n / minPhotos min" with the success colour when reached,
* warning colour otherwise.
*/
thumbnailsMin?: number;
/**
* v0.13.0 — maximum-photos hint for the count line. Renders
* "· maxPhotos max" suffix. No enforcement — the host decides
* what to do at the cap.
*/
thumbnailsMax?: number;
/**
* v0.13.0 — tap handler for thumbnails. When set, replaces the
* strip's built-in tap-to-preview modal; the host shows its own
* preview UI (e.g. with delete / recapture buttons gated on
* sync state). Omit to use the built-in preview.
*/
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
/**
* v0.13.0 — when set, `` renders a built-in `CapturePreview`
* modal as `visible`. Use this for post-stitch confirmation:
* after `onCapture` emits, the host stores the result and sets
* `capturePreview` to the new image, with `capturePreviewActions`
* = `[Discard, Save]` (or similar). Setting `undefined` hides
* the modal.
*
* Hosts using the modal for thumbnail tap-to-preview can leave
* this undefined and let the built-in strip's preview handle
* that case.
*/
capturePreview?: {
imageUri: string;
imageWidth?: number;
imageHeight?: number;
title?: string;
};
/**
* v0.13.0 — action buttons rendered along the bottom of the
* `CapturePreview` modal. Empty array (or undefined) renders
* no buttons, only the close affordance.
*/
capturePreviewActions?: CapturePreviewAction[];
/**
* v0.13.0 — fires when the user dismisses the `capturePreview`
* modal (tap close, backdrop tap, hardware back on Android).
* The host is expected to clear the `capturePreview` prop in
* response.
*/
onCapturePreviewClose?: () => void;
/**
* Optional host-supplied vision-camera frame processor.
*
* ## When to set this prop
*
* v0.8.0+ canonical answer: use the lib's own `useFrameProcessor`
* hook, NOT `react-native-vision-camera`'s. The lib's hook:
*
* - **AR mode**: auto-registers the worklet in the native
* `__stitcherProxy` registry; the AR session's per-frame
* dispatch fans out to it alongside the lib's first-party
* stitching. No prop wiring needed — just mount the hook
* anywhere in the tree.
* - **Non-AR mode**: returns a vc processor object that this
* prop accepts. Wiring it through enables the host's
* worklet to fire on vc's Frame Processor runtime.
*
* ```tsx
* import { Camera, useFrameProcessor, type CameraFrame }
* from 'react-native-image-stitcher';
*
* function MyScreen() {
* const fp = useFrameProcessor((frame: CameraFrame) => {
* 'worklet';
* // ...
* }, []);
* return ;
* }
* ```
*
* ## Non-AR mode composition (v0.11.0+)
*
* vision-camera's `` accepts ONLY ONE frame processor.
* The lib's internal `useFrameProcessorDriver` produces the
* processor that drives first-party panorama stitching in non-AR
* mode. If you supply your own via this prop, the lib's
* default processor is REPLACED — but as of v0.11.0 you can
* COMPOSE first-party stitching back into your worklet body
* using `useStitcherWorklet`:
*
* ```tsx
* import {
* Camera, useFrameProcessor, useStitcherWorklet,
* type CameraFrame,
* } from 'react-native-image-stitcher';
*
* function MyScreen() {
* const stitcher = useStitcherWorklet();
* const fp = useFrameProcessor((frame: CameraFrame) => {
* 'worklet';
* hostPreLogic(frame);
* stitcher.call(frame); // ← first-party stitching
* hostPostLogic(frame);
* }, [stitcher.call]);
* return ;
* }
* ```
*
* Hosts that DON'T call `useStitcherWorklet` from their worklet
* body replace first-party stitching for non-AR captures (a
* one-shot console.info documents this when the prop is first
* supplied). AR mode is unaffected either way — the AR-mode
* dispatch path (v0.8.0 Phase 4b.i / 4b.iii) natively fans out
* to both the lib's first-party stitching AND every registered
* host worklet on every frame, with per-worklet failure
* isolation.
*
* ## AR mode behaviour
*
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
* vc's `` isn't mounted; this prop has no effect.
* Host worklets registered via the lib's `useFrameProcessor`
* fire automatically through the AR-session dispatch path
* (iOS Phase 4b.i / Android Phase 4b.iii).
*
* ## Backwards compatibility
*
* The pre-v0.8.0 behaviour (warn + ignore) is preserved when the
* supplied processor is recognisably from
* `react-native-vision-camera`'s `useFrameProcessor` directly
* (no `__stitcherFrame` marker). Hosts should migrate to the
* lib's `useFrameProcessor` to benefit from AR-mode dispatch.
*
* (v0.5 had a `legacyDriver` escape hatch that routed back to
* `useIncrementalJSDriver`. That hook + prop were removed in
* v0.6 per the deprecation timeline announced in the v0.5.0
* CHANGELOG.)
*/
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
/**
* AR-mode host worklet, invoked once per ARKit / ARCore frame
* ALONGSIDE the lib's first-party stitching (composition, not
* replacement). Receives a `CameraFrame` tagged `source: 'ar'`
* with world-space `pose` + `arTrackingState`. Only fires in AR
* capture (`captureSource === 'ar'`); the non-AR equivalent is
* `frameProcessor` above (the two modes use different runtimes and
* frame shapes). Must be a `'worklet'`-prefixed function; if the
* native install is unavailable it silently never fires.
*/
arFrameProcessor?: CameraFrameProcessor;
/**
* Opt in to per-frame AR depth on the `arFrameProcessor` frame
* (`CameraFrame.arDepth`). Default `false` — depth is the costliest
* field (a per-frame buffer copy), so it's off until you need it.
*/
enableDepth?: boolean;
/**
* Opt in to high-resolution photo capture (iOS 16+, AR capture path).
* When `true`, the AR session runs on the smallest video format that
* supports `captureHighResolutionFrame`, so `takePhoto()` returns a true
* full-res still (for document OCR / detail capture). Default `false` —
* the live stream stays as small as possible; the panorama-stitch path is
* unaffected (its keyframes are downscaled to a fixed budget regardless).
* No-op on Android.
*/
highResCapture?: boolean;
/**
* PANORAMA-QUALITY keyframes (Android; see `ARCameraView` prop of the
* same name): larger ARCore CPU-image config (long-edge ≤ 1920) + a
* lifted keyframe encode budget (640 → 1280) so stitches stop being
* assembled from 0.3 MP tiles on devices whose sole 4:3 config is tiny
* (e.g. Galaxy A35). Costs stitch memory (~4× pixels per keyframe).
* Default `false`. No-op on iOS (native-res keyframes already) and on
* older binaries.
*/
keyframeQualityCapture?: boolean;
/**
* Native-camera 0.5× fallback (Android; default OFF). A list of device
* MODEL identifiers (matched case-insensitively as a PREFIX of
* `Platform.constants.Model`, so `"SM-A346"` covers every A34 SKU) or
* `"manufacturer:"` wildcards, on which the ultra-wide is a
* SYSTEM-ONLY camera unreachable by any third-party app (proven on the
* Galaxy A34). On a matching device that ALSO has no in-app 0.5×
* (`has0_5x=false`), the lens chip renders a "0.5×⤢" pill that fires
* {@link onRequestNativeUltraWide} instead of a dead 1× label — the host
* then hands off to the OS camera. Absent/empty → feature OFF, no change.
* iOS ignores this (its virtual devices already reach the ultra-wide).
*/
nativeUltraWideModels?: readonly string[];
/**
* Fired when the operator taps the "0.5×⤢" native-ultra-wide fallback pill
* (see {@link nativeUltraWideModels}). The host launches the OS camera
* (e.g. `react-native-image-picker` `launchCamera`) and routes the
* returned photo into its own capture flow marked as external provenance —
* the library does NOT launch anything or deliver the external photo.
*/
onRequestNativeUltraWide?: () => void;
/**
* iOS, NON-AR photo path — save each tap photo's AVDepthData as a
* `.depth.bin` sidecar (float32 metres row-major + JSON header
* with dims/intrinsics) and return its path as `depthPath` on the
* photo {@link CameraCaptureResult}. Enables vision-camera depth
* delivery, biases the format pick toward `supportsDepthCapture`
* formats, and extracts the depth BEFORE the orientation re-encode
* strips it. Produces stereo disparity-derived depth on dual-camera
* iPhones and absolute LiDAR-backed depth on Pro models; requires the
* mounted device to be depth-capable (the lens-driven multicam
* selection qualifies — a plain single wide-angle does not). Silently
* yields no sidecar on Android, in AR capture, and on depth-less
* hardware. Distinct from `enableDepth` above, which is the AR
* frame-processor's per-frame depth. Default `false` (depth delivery
* adds per-shot latency).
*/
captureDepthData?: boolean;
/**
* Opt in to per-frame AR anchors (`CameraFrame.arAnchors` — detected
* planes / images). Default `false`.
*/
enableAnchors?: boolean;
/**
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` in
* `arAnchors`, with `meshGeometry`). Default `false`. iOS enables
* ARKit `sceneReconstruction` (LiDAR); Android reconstructs a rough
* mesh from the depth map. Expensive — only on when needed.
*/
enableMesh?: boolean;
/**
* Opt in to the SLAM feature-point cloud in AR plugin contexts. Default
* `false`. Available on ALL AR-capable devices — no LiDAR required.
* Consumed natively by AR plugins only; does not appear in
* {@link ARFrameMeta} or `CameraFrame`.
*
* - iOS → ARKit `rawFeaturePoints` in `RNISARFrameContext.featurePoints`
* as world-space `[simd_float3]` (bare `x, y, z`).
* - Android → ARCore `Frame.acquirePointCloud()` in
* `ARFrameContext.featurePoints` as a flat stride-4
* `[x, y, z, confidence]` world-space `FloatArray` (the extra
* per-point confidence lets native plugins filter ARCore's
* sparser cloud).
*/
enableFeaturePoints?: boolean;
/**
* Which plane orientations to surface in `CameraFrame.arAnchors`
* (requires `enableAnchors`; AR capture only). Default `'vertical'`
* — the orientation the plane-projected stitch path has always used.
* `'horizontal'` surfaces floors / tables; `'both'` surfaces every
* detected plane. See `ARCameraView` for the per-platform details.
*/
planeDetection?: 'vertical' | 'horizontal' | 'both';
/**
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
* MAIN thread (NOT a worklet). Only fires in AR capture
* (`captureSource === 'ar'`). Receives an {@link ARFrameMeta} carrying
* pose, tracking state, intrinsics, and (when the matching `enable*`
* prop is on) depth dimensions, anchors, and mesh counts.
*
* This is the recommended way to read AR metadata: it sidesteps the
* worklet path entirely (the `arFrameProcessor` worklet can only safely
* surface a worklets-core shared value, because capturing a host
* callback crashes the worklet closure-wrap). Native builds the meta
* and emits a device event; `` threads the handler through to
* ``, which subscribes and invokes it on the main thread.
*/
onArFrame?: (meta: ARFrameMeta) => void;
/**
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
*/
arFrameMetaInterval?: number;
/**
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
* framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
* AR capture (`captureSource === 'ar'`). Host-registered native plugins
* (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
* per-frame work to their own queue push results via
* `registry.emit(name, result)`; `` threads this handler to
* ``, which subscribes to the `RNImageStitcherARPluginResult`
* device event and invokes it with `{ plugin, result }`.
*
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
* Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
* the out-of-band async channel — a host can wire either or both.
*
* The SDK ships ONLY the generic plugin framework; there are no built-in
* plugins, so this never fires unless the host registers native plugins.
*/
onArPluginResult?: (e: ARPluginResult) => void;
/**
* v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
* shapes drawn ON TOP of the AR camera preview, each anchored to WORLD
* positions and REPROJECTED to screen on every AR frame from the current
* camera pose + intrinsics (smooth display-rate tracking, no 3D engine).
* Only meaningful in AR capture (`captureSource === 'ar'`); ``
* threads this straight through to the underlying ``.
*
* State-driven: pass a React-state array and update it as your world points
* change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is
* diffed against the current overlays BY `id`. For zero-render-latency
* mutations use the imperative ref methods on the `` handle instead
* ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` /
* `removeOverlay` / `clearOverlays`) — both paths funnel through the same
* native channel. JS-set overlays merge on the native side with overlays a
* registered AR plugin placed directly (namespaced so neither clobbers the
* other). See {@link AROverlay} for the shape.
*/
overlays?: AROverlay[];
/**
* Which device holds the non-AR panorama capture accepts.
*
* - `'vertical'` (DEFAULT) — LANDSCAPE-only, top→bottom pan. Starting a
* panorama in portrait is BLOCKED behind the rotate-to-landscape
* prompt (item 2); the capture starts the instant they rotate to
* landscape (either way up).
* - `'horizontal'` — PORTRAIT-only, left→right pan. Starting in
* landscape is BLOCKED behind the rotate-to-portrait prompt; capture
* starts on rotating to portrait (either way up).
* - `'both'` — landscape OR portrait; the rotate gate never fires, the
* user captures in whichever hold they're already in.
*
* **BREAKING (since the previous release accepted both holds ungated):**
* the default is now `'vertical'`. Hosts that want left→right (portrait)
* panoramas use `panMode='horizontal'` (portrait-only) or `'both'`. See
* CHANGELOG.
*/
panMode?: PanMode;
/**
* Master switch for the in-capture pan-guidance surfaces (rotate
* prompt, pan how-to overlay, too-fast pill, blinking countdown).
* Default `true`. Set `false` to suppress all of them (the lateral-
* drift FINALIZE behaviour and the crop preview are governed by their
* own props, not this flag).
*/
panGuidance?: boolean;
/**
* Optional hard recording-TIME ceiling for a non-AR panorama, in
* milliseconds, used as a SAFETY cap alongside the primary keyframe-count
* auto-stop. The default capture now finalizes when the configured
* keyframe count is reached (see the frame counter HUD), so this is `0`
* (disabled) by default. Set it to a positive value to ALSO cap the
* recording by wall-clock time; when > 0 a blinking countdown (item 5)
* shows the seconds remaining and the capture auto-finalizes at 0.
*
* v0.16 — default changed `9000` → `0` (time cap is now opt-in; the
* keyframe-count stop is the default UX).
*/
maxPanDurationMs?: number;
/**
* Gyro rate (rad/s) above which the pan is flagged "moving too fast"
* (item 4 — the transient amber pill). Optional; forwards to
* `usePanMotion`'s `warnMaxRadPerSec` (default 1.0 rad/s there).
*/
panTooFastThreshold?: number;
/**
* Cross-pan (lateral) drift budget in CENTIMETRES (item 6). Once the
* operator's integrated sideways translation exceeds this for the
* hook's grace window, the capture is STOPPED.
*
* **Default `8`** (v0.25.3 — was `4`). `0` disables the lateral-drift
* stop entirely.
*
* This is the SENSITIVITY knob: it decides how much sideways drift is
* tolerated before a capture is stopped at all. What HAPPENS at that
* stop — finalize the partial sweep, or discard it — is a separate
* decision controlled by `lateralStopFinalizeMinFrames`. The two are
* easy to confuse when tuning: if operators complain the stop fires
* too eagerly, raise this; if they complain that stopped captures are
* thrown away, lower that one.
*
* v0.25.3 raised the default from `4` after field reports of the stop
* firing on minor drift. 4 cm of integrated sideways translation is
* a small movement to hold to over a hand-held sweep — comfortably
* inside the natural arc of pivoting on the spot — so it tripped on
* captures the operator considered fine. The detector itself is
* unchanged; only the budget it is measured against moved.
*/
lateralBudgetCm?: number;
/**
* Cross-pan ROTATION rate, rad/s, above which the capture is stopped
* for lateral drift. Defaults to `DEFAULT_LATERAL_TURN_RAD_PER_SEC`
* (0.15 rad/s ≈ 8.6 °/s) — unset reproduces today's behaviour.
*
* `lateralBudgetCm` is NOT the only lateral trigger. This gyro EMA
* is a second, independent one, and historically the primary. A stop
* you attribute to "drifting sideways" may be this rotation trigger
* instead — check `latch=gyro|accel` in the `[panMotion]` telemetry
* (see `panMotionDebug`) before tuning either number.
*
* `0` (or negative) disables THIS trigger only; `lateralBudgetCm={0}`
* disables both.
*/
lateralTurnRateRadPerSec?: number;
/**
* Continuous over-threshold dwell, ms, before the ROTATION trigger stops the
* capture. Default 500 ms — matching the displacement trigger, which has
* always had one.
*
* Before v0.26.0 the rotation trigger latched on the FIRST sample over
* threshold, so one brief wobble ended a capture permanently. `0` comes as
* close to restoring that as the shared latch helper allows — it still costs
* one gyro sample (~33 ms), because the dwell clock starts on the first
* over-threshold sample and latches only on a later one.
*/
lateralTurnGraceMs?: number;
/**
* Absolute cross-pan ANGLE, DEGREES, at which the capture is stopped.
* Default 25; `0` disables while still measuring. Works in BOTH AR and
* non-AR (gyro-integrated), unlike `arLateralRotDeg` which needs the pose.
*
* Catches the slow pivot `lateralTurnRateRadPerSec` cannot: that is a rate
* gate, so 6 deg/s turns 90 degrees over 15 s without tripping it.
*/
lateralTurnAngleDeg?: number;
/**
* ABSOLUTE cross-pan drift budget in CENTIMETRES, measured from the AR
* camera POSE. AR captures only. Default 8 cm; `0` disables the stop
* while still measuring and logging the distance.
*
* A genuine displacement, unlike `lateralBudgetCm`, which gates a
* high-passed rate proxy. Recovering distance from an accelerometer needs
* double integration, whose bias growth must be high-passed away — and that
* same high-pass removes slow real motion, so the IMU guard structurally
* cannot see slow sideways drift however it is tuned. ARKit's VIO position
* needs no integration, so it can; and tilt cannot masquerade as
* translation, because a position is not an acceleration.
*
* The axis is chosen so the sweep's own ARC never projects onto it — a
* vertical pan measures the horizontal cross-view direction, a horizontal
* pan measures world up — so an operator pivoting cleanly in place reads ~0
* regardless of sweep angle or arm length (measured < 5 mm over an 0.8 rad
* sweep at pivot radii of 15-60 cm).
*/
arLateralBudgetCm?: number;
/**
* ABSOLUTE cross-pan ROTATION budget in DEGREES, from the AR camera pose.
* AR captures only. Default 25 deg; `0` disables the stop while still
* measuring.
*
* Closes the slow-pivot hole. `lateralTurnRateRadPerSec` is a RATE gate
* (0.15 rad/s = 8.6 deg/s), so it cannot see a slow turn however far it
* goes: 6 deg/s accumulates 90 DEGREES of yaw over 15 s and never trips it.
* That is the rotation twin of the slow-translation blind spot — a rate gate
* measures how FAST you turn, never how FAR you have turned.
*
* Measured on the camera's forward VECTOR, so the intended sweep contributes
* nothing (a vertical pan measures azimuth, a horizontal pan elevation) and
* ROLL about the view axis contributes nothing either — roll does not change
* where the camera points, and it is already the motion that corrupted the
* accelerometer channel.
*/
arLateralRotDeg?: number;
/**
* Cross-pan allowance as a RATIO of along-pan travel (AR only).
*
* A fixed centimetre budget cannot distinguish 6 cm of drift across a 60 cm
* sweep (10 % — still overwhelmingly a pan, and about as straight as a hand
* gets over half a metre) from the same 6 cm across a 10 cm one (60 % — not
* a pan at all). The effective budget is therefore
* `clamp(ratio * alongPanDistance, arLateralBudgetCm, arLateralMaxCm)`.
*
* `arLateralBudgetCm` becomes the FLOOR: short sweeps, and the opening of
* every capture where along-pan travel is still ~0, keep exactly the old
* absolute behaviour. `<= 0` disables the proportional term entirely.
*/
arLateralRatio?: number;
/** Ceiling on the ratio allowance, CENTIMETRES. `<= 0` = uncapped. */
arLateralMaxCm?: number;
/**
* Which lateral-drift physics to run. Default `'fused'`.
*
* `'fused'` subtracts the device's FUSED GRAVITY SENSOR from each
* accelerometer sample and derives the integration step from the
* sample's own timestamp. `'legacy'` restores the pre-0.25.4
* behaviour bit-for-bit: an IIR gravity estimate and a hardcoded
* 20 ms step.
*
* You almost certainly want the default. The legacy estimator cannot
* distinguish a wrist TILT from a sideways SLIDE — a change in how
* gravity projects onto the cross-pan axis is arithmetically
* identical to real lateral acceleration — so it fabricates ~1.1 cm
* of drift per degree of net tilt and latches the stop on ordinary
* hand movement. This prop exists as an ESCAPE HATCH so a host that
* hits an unexpected device-specific regression can back out without
* pinning an old version of the library, not as an opt-in gate.
*
* Degrades on its own: if the device has no fused gravity sensor, or
* it stops delivering mid-capture, the hook falls back to the legacy
* estimator automatically for exactly as long as it needs to.
*/
lateralMotionModel?: LateralMotionModel;
/**
* Emit the throttled `[panMotion.*]` diagnostic logs. Default
* `__DEV__`, i.e. unset behaves exactly as before. Set `true` to keep
* them in a release build while diagnosing a field report — they are
* the intended instrument for tuning `lateralBudgetCm` against real
* captures.
*/
panMotionDebug?: boolean;
/**
* The accepted-keyframe count at or above which a lateral-drift stop
* (item 6) FINALIZES the capture: the partial sweep is stitched and handed
* to `onCapture` with a `LATERAL_DRIFT_FINALIZE` warning. BELOW it the
* capture is DISCARDED instead — the engine is cancelled, nothing is
* stitched, and `onCaptureAbandoned('lateral-drift')` fires.
*
* **Default `5`. This is a BEHAVIOUR CHANGE, not a no-op** — the SDK used
* to hardcode `2`, so captures that accepted 2-4 keyframes previously
* finalized and now DISCARD. That is intentional: a 2-to-4-frame remnant
* of a sweep the operator drifted out of is not a usable shelf panorama,
* and asking for a clean re-shoot beats handing a host output it has to
* detect and reject downstream. **Pass `2` to restore the old
* behaviour exactly.**
*
* - `0` — **ALWAYS DISCARD**, however many keyframes were accepted. This
* is a genuine special case and NOT the `count >= 0` the arithmetic
* would otherwise give you (that is unconditionally true, i.e. the
* opposite). For hosts whose downstream pipeline treats any drifted
* sweep as garbage, discarding costs nothing and saves the stitch, the
* file, and the operator's attention on output they will bin anyway.
* - `N >= 1` — finalize iff `acceptedKeyframeCount >= N`.
* - `2` — the pre-policy behaviour: keep anything stitchable.
*
* Negative, `NaN` and infinite values normalise to the default, so a broken
* host config degrades to the standard threshold rather than to "throw
* every capture away"; fractional values round UP, as the prop counts whole
* frames.
*
* The discard path shows a THIRD popup state whose copy does not promise a
* stitch — `lateralStopDiscardedTitle` / `lateralStopDiscardedBody` in
* {@link guidanceCopy}. At the default that state covers the 2-to-4
* keyframe band. Below 2 accepted keyframes nothing stitchable was
* captured at all, so the popup keeps the existing "follow the arrow"
* wrong-direction copy regardless of this threshold.
*/
lateralStopFinalizeMinFrames?: number;
/**
* v0.25 — whether a mid-capture device rotation auto-ABANDONS the
* in-flight panorama (the OrientationDriftModal explains it to the
* user). Default `true` (the behaviour since v0.12). Set `false`
* to disable the guard entirely: the capture then continues across a
* rotation and the output is best-effort (cross-mode captures can
* stitch malformed — see `incremental.ts` stitch-mode notes).
*
* The detector itself is sensor-trust hardened as of v0.25: it never
* snapshots or compares orientation until the accelerometer has
* delivered a real sample, so hosts with broken/laggy
* react-native-sensors delivery no longer see phantom "rotation"
* abandons (field RCA: landscape captures auto-abandoning after one
* frame while portrait worked).
*/
orientationDriftAbandon?: boolean;
/**
* v0.25 — the keyframe count below which a finished capture is flagged
* with the `CAPTURE_TOO_SHORT` capture WARNING. **Default `1`, which
* never warns** and reproduces the previous behaviour exactly.
*
* Set `2` to be told when a capture produced a single frame. The
* capture still SUCCEEDS and still returns that frame — a one-shot
* capture is a legitimate result — but it is no longer silent. That
* silence is why the AR self-ending-hold failures were reported as
* stitching bugs: the SDK returned the lone frame as an ordinary
* panorama with `singleKeyframe: true` that nothing read.
*
* Evaluated from the FINALIZE RESULT, not from the live accepted count.
* The live count omits any keyframe whose anti-blur sharpness window is
* still open at release — the trailing keyframe of nearly every capture
* — and it means different things on iOS and Android, so judging
* "too short" from it would misfire constantly. An earlier draft of
* this feature did exactly that and would have destroyed valid
* captures; adversarial review caught it.
*/
minPanoramaKeyframes?: number;
/**
* Show the draggable-quad crop editor after a panorama finalizes, BEFORE
* emitting it via `onCapture`. Default `false`. When `true`, the user
* drags 4 corners over the stitched result; confirming crops in place
* (perspective-rectify when the quad isn't axis-aligned), "Use original"
* emits the un-cropped panorama, "Retake" discards it. Takes precedence
* over {@link showPreview}.
*/
rectCrop?: boolean;
/**
* Show a plain review screen after a panorama finalizes — the stitched
* image with [Retake] / [Confirm] and NO crop box. Default `false`.
* Ignored when {@link rectCrop} is on (the crop editor is itself the
* preview). With both off, `onCapture` fires immediately with no UI.
*/
showPreview?: boolean;
/**
* Copy overrides for every guidance string (rotate prompt, pan hint,
* too-fast warning, lateral-stop popup, crop buttons). Partial —
* unspecified keys fall back to {@link DEFAULT_GUIDANCE_COPY}. Hosts
* localise or re-word the whole guidance surface in one place here.
*/
guidanceCopy?: Partial;
}
/**
* v0.20.0 — imperative handle exposed via the `` ref.
*
* Currently scoped to the AR-overlay methods ({@link AROverlayMethods}:
* `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
* `clearOverlays`), which forward to the underlying ``'s overlay
* channel when AR mode is mounted. They are no-ops while the camera is in
* non-AR mode (no `` is mounted, and overlays only render over
* the AR preview) — use the declarative {@link CameraProps.overlays} prop for
* a set that survives AR↔non-AR transitions, since it re-applies automatically
* whenever `` (re)mounts.
*
* The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a
* host can use either component with the same overlay code. Panorama capture
* remains driven by the built-in shutter; single-photo capture can also be
* triggered imperatively via {@link CameraHandle.takePhoto} (added 0.20.5 for
* hands-free / auto-capture flows like the document scanner).
*/
export interface CameraHandle extends AROverlayMethods {
/**
* Imperatively fire a single-photo capture — identical to the user tapping
* the shutter (same AR / non-AR routing, same `onCapture` callback, same
* output path rules). Respects `enablePhotoMode` and `shutterDisabled`: a
* no-op while photo mode is off or the shutter is gated, so callers can fire
* it freely and let the gate decide. Resolves once the capture attempt
* settles (success or handled error reported through `onCapture`).
*/
takePhoto(): Promise;
/**
* Imperatively START a panorama sweep — identical to the user beginning a
* hold on the shutter (the incremental stitcher starts ingesting AR frames).
* A no-op unless `enablePanoramaMode` is on and the shutter is not gated
* (`shutterDisabled`), and while a capture is already recording/stitching —
* the same gates `takePhoto` respects. Pair with {@link stopPanorama}.
* Added for hosts that render their OWN shutter (see `hideBuiltInShutter`)
* and drive capture through this handle instead of the built-in button.
*/
startPanorama(): void;
/**
* Imperatively STOP an in-flight panorama sweep — identical to releasing the
* shutter hold: finalize the stitch and emit `onCapture`. Idempotent (a safe
* no-op when nothing is recording). Resolves once the stop is dispatched.
*/
stopPanorama(): Promise;
/**
* Set the preferred capture source — for hosts that render their own chrome.
*
* `hideBuiltInShutter` hides the built-in shutter AND the AR toggle, on the
* documented premise that the host draws its own capture controls. But
* before v0.26.0 the host had no way to DRIVE the AR toggle: `arPreference`
* was internal state with no prop and no handle method. So such a host
* could replace the shutter (this handle covers capture) and could NOT
* replace the AR control — leaving it locked to whatever
* `defaultCaptureSource` resolved to at mount, with no indicator and no
* switch. That is exactly what shipped in a field build.
*
* Sets the same PREFERENCE the built-in pill flips, so host chrome and the
* built-in control are interchangeable rather than competing sources of
* truth. The EFFECTIVE source is still clamped by `captureSources`, by
* device AR support, and by the 0.5x lens (ARKit/ARCore cannot drive the
* ultra-wide) — so requesting `'ar'` on an unsupported device stays non-AR.
* Subscribe to `onCaptureSourceChange` for what actually took effect.
*/
setCaptureSource(source: CaptureSource): void;
}
/**
* The public `` component.
*
* v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
* overlay methods); existing callers that don't pass a ref are unaffected
* (`forwardRef` makes the ref optional).
*/
export declare const Camera: React.ForwardRefExoticComponent>;
/**
* v0.12.0 — JS edge corresponding to the physical home-indicator
* side of the device. This is where the shutter + controls anchor
* to so they're always within thumb reach of the user's grip
* (matching iOS Camera's behaviour).
*
* Combines two signals:
* - `jsLandscape`: whether the OS rotated the framebuffer. True
* only for non-locked hosts in device-landscape.
* - `deviceOrient`: physical device orientation from the sensor.
*
* Truth table:
* | jsLandscape | deviceOrient | edge |
* |--- |--- |--- |
* | false | any | bottom | (portrait JS coords —
* | | | | device-bottom = JS-bottom
* | | | | in both locked and
* | | | | non-locked-portrait)
* | true | landscape-left | right | (screen rotated, home
* | | | | indicator on user-right)
* | true | landscape-right | left | (mirror)
*
* Caveats:
* - Non-locked + upside-down doesn't surface JS-top here because
* upside-down doesn't change window dimensions; we can't
* distinguish locked-portrait-with-device-flipped from
* non-locked-portrait-with-screen-flipped-180°. Defaults to
* JS-bottom which matches the more common locked case. Add
* handling here when a host needs upside-down support.
* - jsLandscape=true with non-landscape device shouldn't happen
* in steady state — only during a transition mid-rotation.
* Falls through to 'right' as a defensive default.
*/
type HomeIndicatorEdge = 'bottom' | 'top' | 'left' | 'right';
declare function homeIndicatorEdge(jsLandscape: boolean, deviceOrient: DeviceOrientation): HomeIndicatorEdge;
/**
* v0.12.0 — true when the anchor edge is on a side (left/right), so
* the band + shutter row need to be vertical strips. Top/bottom
* anchors yield horizontal strips.
*/
declare function isSideEdge(edge: HomeIndicatorEdge): boolean;
/** @internal test-only — see `homeIndicatorEdge`. */
export declare const _homeIndicatorEdgeForTests: typeof homeIndicatorEdge;
/** @internal test-only — see `isSideEdge`. */
export declare const _isSideEdgeForTests: typeof isSideEdge;
/**
* cameraShouldUnmount — whether the live camera ( /
* ) should be UNMOUNTED (replaced by the placeholder) this
* render rather than mounted.
*
* True while a camera-switch transition or AR-support probe is in flight,
* OR during the stitch (statusPhase==='stitching'). The stitching case is
* the V12.14.8 OOM fix: unmounting frees vision-camera's AVCaptureSession +
* preview buffers (~150-250 MB) BEFORE the memory-heavy stitch, so the
* live-camera footprint and the stitch peak never coexist and jetsam (iOS)
* / lmkd (Android) don't OOM-kill the app.
*
* Pure + exported for test — the lib's jest config can't mount ,
* so this boolean is the unit-testable core of the OOM render gate.
*/
declare function cameraShouldUnmount(inFlightTransition: boolean, arSupportPending: boolean, statusPhase: CaptureStatusPhase): boolean;
/** @internal test-only — see `cameraShouldUnmount`. */
export declare const _cameraShouldUnmountForTests: typeof cameraShouldUnmount;
/**
* v0.25 — must a hold DEFER because there is no camera to capture from?
*
* This is deliberately the first two terms of `cameraShouldUnmount`
* above, and that is the whole point: the render gate and the hold gate
* were reading different conditions, so a hold could start a capture
* against a camera the renderer had just deliberately unmounted.
*
* The hole this closes: `arSupportPending` clears in the SAME render
* that flips `isAR` false→true, which makes `inFlightTransition` true,
* unmounts the camera and (on iOS) stops the AR session with a 250 ms
* grace before the AR view may mount again. The v0.24.3 defer resumed
* on `!arSupportPending` alone — i.e. at exactly the moment the
* transition BEGAN — so the resumed capture ran against no frame source
* and finalized with "0 keyframes saved".
*
* `statusPhase === 'stitching'` is intentionally NOT included:
* `handleHoldStart` already rejects that phase outright rather than
* queueing a deferred start.
*/
declare function holdShouldDeferForCamera(inFlightTransition: boolean, arSupportPending: boolean): boolean;
/** @internal test-only — see `holdShouldDeferForCamera`. */
export declare const _holdShouldDeferForCameraForTests: typeof holdShouldDeferForCamera;
export {};
//# sourceMappingURL=Camera.d.ts.map