diff --git a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts
index df49bb6378..6c186ec479 100644
--- a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts
+++ b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts
@@ -94,6 +94,8 @@ describe('VisionCamera - Devices', () => {
it('reports sane capability invariants for each device', () => {
for (const device of factory.cameraDevices) {
expect(device.minZoom).toBeLessThanOrEqual(device.maxZoom)
+ expect(device.neutralZoom).toBeGreaterThanOrEqual(device.minZoom)
+ expect(device.neutralZoom).toBeLessThanOrEqual(device.maxZoom)
if (device.supportsExposureBias) {
expect(device.minExposureBias).toBeLessThanOrEqual(
diff --git a/docs/content/docs/zooming.mdx b/docs/content/docs/zooming.mdx
index 686aa49c99..ae3132676a 100644
--- a/docs/content/docs/zooming.mdx
+++ b/docs/content/docs/zooming.mdx
@@ -13,7 +13,7 @@ The [``](/api/react-native-vision-camera/views/Camera) view allows adj
```tsx
function App() {
// [!code ++]
- const zoom = useSharedValue(device.minZoom)
+ const zoom = useSharedValue(device.neutralZoom)
return (
@@ -97,12 +97,15 @@ for (const switchOverFactor of device.zoomLensSwitchFactors) {
#### Default Zoom
-To provide a natural user experience, always start at zoom `1`.
-If a virtual Camera contains an [`'ultra-wide-angle'`](/api/react-native-vision-camera/type-aliases/DeviceType) Camera (the "0.5x Camera"), the user may zoom out to less than `1` (here `0.5`) instead.
+To provide a natural user experience, start at the Camera's [`neutralZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#neutralzoom).
+This is the internal zoom factor that appears as `1x` to the user and selects the standard wide-angle lens.
-- [`minZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#minzoom) <= `1` (e.g. 0.5x)
-- natural zoom = `1`
-- [`maxZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#maxzoom) >= `1` (e.g. 3x)
+On most Cameras `neutralZoom` is `1`, but iOS virtual Cameras that contain an [`'ultra-wide-angle'`](/api/react-native-vision-camera/type-aliases/DeviceType) lens can use a larger internal factor, such as `2`.
+In that case the user can still zoom out below `neutralZoom` to reach the ultra-wide-angle lens.
+
+- [`minZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#minzoom) <= [`neutralZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#neutralzoom)
+- natural zoom = [`neutralZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#neutralzoom) (displayed as `1x`)
+- [`maxZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#maxzoom) >= [`neutralZoom`](/api/react-native-vision-camera/hybrid-objects/CameraDevice#neutralzoom)
### Getting current zoom values
diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt
index a00188fb15..7d08a62c8a 100644
--- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt
+++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt
@@ -194,6 +194,7 @@ class HybridCameraDevice(
val zoomState = cameraInfo.zoomState.value ?: return 0.0
return zoomState.maxZoomRatio.toDouble()
}
+ override val neutralZoom: Double = 1.0
override val zoomLensSwitchFactors: DoubleArray
get() = cameraInfo.zoomLensSwitchFactors
diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt
index ef1d9b73f0..8e24861a63 100644
--- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt
+++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt
@@ -92,6 +92,7 @@ class HybridPhysicalCameraDevice(
override val supportsLowLightBoost: Boolean = false
override val minZoom: Double = 0.0
override val maxZoom: Double = 0.0
+ override val neutralZoom: Double = 1.0
override val zoomLensSwitchFactors: DoubleArray = doubleArrayOf()
override val supportsDistortionCorrection: Boolean = false
diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+neutralZoom.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+neutralZoom.swift
new file mode 100644
index 0000000000..cde83d8133
--- /dev/null
+++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+neutralZoom.swift
@@ -0,0 +1,40 @@
+///
+/// AVCaptureDevice+neutralZoom.swift
+/// VisionCamera
+/// Copyright © 2026 Marc Rousavy @ Margelo
+///
+
+import AVFoundation
+import Foundation
+
+extension AVCaptureDevice {
+ /**
+ * The internal video zoom factor that AVFoundation displays as `1x`.
+ *
+ * On older iOS versions, derive it from the switchover factor immediately
+ * before the standard wide-angle constituent Camera.
+ */
+ var neutralZoomFactor: Double {
+ if #available(iOS 18.0, *) {
+ let multiplier = displayVideoZoomFactorMultiplier
+ if multiplier > 0 {
+ return 1 / multiplier
+ }
+ }
+
+ guard
+ let wideAngleIndex = constituentDevices.firstIndex(where: {
+ $0.deviceType == .builtInWideAngleCamera
+ }),
+ wideAngleIndex > 0
+ else {
+ return 1
+ }
+
+ let switchoverIndex = wideAngleIndex - 1
+ guard virtualDeviceSwitchOverVideoZoomFactors.indices.contains(switchoverIndex) else {
+ return 1
+ }
+ return virtualDeviceSwitchOverVideoZoomFactors[switchoverIndex].doubleValue
+ }
+}
diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift
index bf7bafae85..df9d4a8d43 100644
--- a/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift
+++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift
@@ -221,6 +221,10 @@ final class HybridCameraDevice: HybridCameraDeviceSpec, NativeCameraDevice {
return device.maxAvailableVideoZoomFactor
}
+ var neutralZoom: Double {
+ return device.neutralZoomFactor
+ }
+
var zoomLensSwitchFactors: [Double] {
return device.virtualDeviceSwitchOverVideoZoomFactors.map { $0.doubleValue }
}
diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp
index e6f0189fbc..6b8c17ba76 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp
+++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp
@@ -338,6 +338,11 @@ namespace margelo::nitro::camera {
auto __result = method(_javaPart);
return __result;
}
+ double JHybridCameraDeviceSpec::getNeutralZoom() {
+ static const auto method = _javaPart->javaClassStatic()->getMethod("getNeutralZoom");
+ auto __result = method(_javaPart);
+ return __result;
+ }
std::vector JHybridCameraDeviceSpec::getZoomLensSwitchFactors() {
static const auto method = _javaPart->javaClassStatic()->getMethod()>("getZoomLensSwitchFactors");
auto __result = method(_javaPart);
diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp
index d80aeffe1b..11f43eb718 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp
+++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp
@@ -88,6 +88,7 @@ namespace margelo::nitro::camera {
bool getSupportsLowLightBoost() override;
double getMinZoom() override;
double getMaxZoom() override;
+ double getNeutralZoom() override;
std::vector getZoomLensSwitchFactors() override;
bool getSupportsDistortionCorrection() override;
diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt
index 0cf2ca167f..706be44e50 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt
+++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt
@@ -178,6 +178,10 @@ abstract class HybridCameraDeviceSpec: HybridObject() {
@get:Keep
abstract val maxZoom: Double
+ @get:DoNotStrip
+ @get:Keep
+ abstract val neutralZoom: Double
+
@get:DoNotStrip
@get:Keep
abstract val zoomLensSwitchFactors: DoubleArray
diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp
index d8518138ab..a6c4dd2f69 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp
+++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp
@@ -236,6 +236,9 @@ namespace margelo::nitro::camera {
inline double getMaxZoom() noexcept override {
return _swiftPart.getMaxZoom();
}
+ inline double getNeutralZoom() noexcept override {
+ return _swiftPart.getNeutralZoom();
+ }
inline std::vector getZoomLensSwitchFactors() noexcept override {
auto __result = _swiftPart.getZoomLensSwitchFactors();
return __result;
diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift
index cee3f745f9..ddcce82164 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift
+++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift
@@ -48,6 +48,7 @@ public protocol HybridCameraDeviceSpec_protocol: HybridObject {
var supportsLowLightBoost: Bool { get }
var minZoom: Double { get }
var maxZoom: Double { get }
+ var neutralZoom: Double { get }
var zoomLensSwitchFactors: [Double] { get }
var supportsDistortionCorrection: Bool { get }
diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift
index 2269e3df93..547324ca36 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift
+++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift
@@ -435,6 +435,13 @@ open class HybridCameraDeviceSpec_cxx {
}
}
+ public final var neutralZoom: Double {
+ @inline(__always)
+ get {
+ return self.__implementation.neutralZoom
+ }
+ }
+
public final var zoomLensSwitchFactors: bridge.std__vector_double_ {
@inline(__always)
get {
diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp
index e0c9926771..5ac9bc711f 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp
+++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp
@@ -52,6 +52,7 @@ namespace margelo::nitro::camera {
prototype.registerHybridGetter("supportsLowLightBoost", &HybridCameraDeviceSpec::getSupportsLowLightBoost);
prototype.registerHybridGetter("minZoom", &HybridCameraDeviceSpec::getMinZoom);
prototype.registerHybridGetter("maxZoom", &HybridCameraDeviceSpec::getMaxZoom);
+ prototype.registerHybridGetter("neutralZoom", &HybridCameraDeviceSpec::getNeutralZoom);
prototype.registerHybridGetter("zoomLensSwitchFactors", &HybridCameraDeviceSpec::getZoomLensSwitchFactors);
prototype.registerHybridGetter("supportsDistortionCorrection", &HybridCameraDeviceSpec::getSupportsDistortionCorrection);
prototype.registerHybridMethod("getSupportedResolutions", &HybridCameraDeviceSpec::getSupportedResolutions);
diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp
index df4ecd4e0b..3da17ffe8a 100644
--- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp
+++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp
@@ -120,6 +120,7 @@ namespace margelo::nitro::camera {
virtual bool getSupportsLowLightBoost() = 0;
virtual double getMinZoom() = 0;
virtual double getMaxZoom() = 0;
+ virtual double getNeutralZoom() = 0;
virtual std::vector getZoomLensSwitchFactors() = 0;
virtual bool getSupportsDistortionCorrection() = 0;
diff --git a/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts b/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts
index 6f984fc4e9..411f5e1130 100644
--- a/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts
+++ b/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts
@@ -578,6 +578,19 @@ export interface CameraDevice
* for a true current maximum.
*/
readonly maxZoom: number
+ /**
+ * The zoom factor at which this Camera appears naturally zoomed (`1x`)
+ * to the user.
+ *
+ * For physical Cameras and Android devices this is typically `1.0`.
+ * On iOS virtual Cameras that include an ultra-wide-angle lens, this can
+ * be greater than `1.0` because AVFoundation's internal zoom scale starts
+ * at the widest constituent lens.
+ *
+ * Use this value as the initial zoom if the Camera should start on its
+ * standard wide-angle lens while still allowing the user to zoom out.
+ */
+ readonly neutralZoom: number
/**
* If this {@linkcode CameraDevice} is a virtual device,
* this returns a list of zoom factors at which the virtual