diff --git a/frontend/libs/components/video-editor/src/lib/components/ui/number-field.spec.tsx b/frontend/libs/components/video-editor/src/lib/components/ui/number-field.spec.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/video-editor/src/lib/components/ui/number-field.spec.tsx
@@ -0,0 +1,141 @@
+import { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { NumberField } from "./number-field";
+
+const ORIGINAL_POINTER_LOCK_ELEMENT = Object.getOwnPropertyDescriptor(document, "pointerLockElement");
+const ORIGINAL_EXIT_POINTER_LOCK = Object.getOwnPropertyDescriptor(document, "exitPointerLock");
+const ORIGINAL_REQUEST_POINTER_LOCK = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "requestPointerLock");
+const ORIGINAL_ACT_ENVIRONMENT = Object.getOwnPropertyDescriptor(globalThis, "IS_REACT_ACT_ENVIRONMENT");
+
+const mounted: Array<{ root: Root; node: HTMLDivElement }> = [];
+let pointerLockOwner: Element | null;
+let exitPointerLock: ReturnType<typeof vi.fn>;
+
+vi.mock("../../utils/ui", () => ({ cn: (...values: unknown[]) => values.filter(Boolean).join(" ") }));
+vi.mock("./button", () => ({ Button: () => null }));
+
+beforeEach(() => {
+  pointerLockOwner = null;
+  exitPointerLock = vi.fn(() => { pointerLockOwner = null; });
+  Object.defineProperty(document, "pointerLockElement", { configurable: true, get: () => pointerLockOwner });
+  Object.defineProperty(document, "exitPointerLock", { configurable: true, value: exitPointerLock });
+  Object.defineProperty(HTMLElement.prototype, "requestPointerLock", { configurable: true, value: function () { pointerLockOwner = this; } });
+  (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(() => {
+  for (const fixture of mounted.splice(0)) {
+    act(() => fixture.root.unmount());
+    fixture.node.remove();
+  }
+  // Also release a leaked baseline session, so it cannot contaminate another test.
+  document.dispatchEvent(new MouseEvent("pointerup"));
+  restoreDescriptor(document, "pointerLockElement", ORIGINAL_POINTER_LOCK_ELEMENT);
+  restoreDescriptor(document, "exitPointerLock", ORIGINAL_EXIT_POINTER_LOCK);
+  restoreDescriptor(HTMLElement.prototype, "requestPointerLock", ORIGINAL_REQUEST_POINTER_LOCK);
+  restoreDescriptor(globalThis, "IS_REACT_ACT_ENVIRONMENT", ORIGINAL_ACT_ENVIRONMENT);
+  vi.restoreAllMocks();
+});
+
+it("stops property previews and releases its pointer lock when the field disappears", () => {
+  const fixture = mountField();
+  fixture.start();
+  move(0);
+  move(3);
+  expect(fixture.values).toEqual([13]);
+  act(() => fixture.root.unmount());
+  const lockAfterUnmount = pointerLockOwner;
+  move(7);
+  expect(fixture.values).toEqual([13]);
+  expect(lockAfterUnmount).toBeNull();
+  expect(fixture.commits).not.toHaveBeenCalled();
+});
+
+it("ends the scrub once when Escape releases pointer lock", () => {
+  const fixture = mountField();
+  fixture.start();
+  move(0);
+  move(3);
+  pointerLockOwner = null;
+  act(() => document.dispatchEvent(new Event("pointerlockchange")));
+  move(7);
+  act(() => document.dispatchEvent(new MouseEvent("pointerup")));
+  expect(fixture.values).toEqual([13]);
+  expect(fixture.commits).toHaveBeenCalledTimes(1);
+});
+
+it("ends the scrub when the window loses focus before pointerup", () => {
+  const fixture = mountField();
+  fixture.start();
+  move(0);
+  move(3);
+  act(() => window.dispatchEvent(new Event("blur")));
+  move(7);
+  expect(fixture.values).toEqual([13]);
+  expect(fixture.commits).toHaveBeenCalledTimes(1);
+  expect(pointerLockOwner).toBeNull();
+});
+
+it("does not release a different control's pointer lock when the field disappears", () => {
+  const fixture = mountField();
+  fixture.start();
+  const otherOwner = document.createElement("button");
+  pointerLockOwner = otherOwner;
+  act(() => fixture.root.unmount());
+  act(() => document.dispatchEvent(new MouseEvent("pointerup")));
+  expect(exitPointerLock).not.toHaveBeenCalled();
+  expect(pointerLockOwner).toBe(otherOwner);
+});
+
+it("keeps ordinary scrubbing and commits exactly once on pointerup", () => {
+  const fixture = mountField();
+  fixture.start();
+  move(99); // Ignore the first pointer-lock warp delta, as in the live field.
+  move(3);
+  move(-1);
+  act(() => document.dispatchEvent(new MouseEvent("pointerup")));
+  move(100);
+  expect(fixture.values).toEqual([13, 12]);
+  expect(fixture.commits).toHaveBeenCalledTimes(1);
+  expect(pointerLockOwner).toBeNull();
+});
+
+it("keeps disabled and non-primary pointer actions inert", () => {
+  const fixture = mountField(true);
+  fixture.start();
+  move(0);
+  move(3);
+  expect(fixture.values).toEqual([]);
+  expect(fixture.commits).not.toHaveBeenCalled();
+  expect(pointerLockOwner).toBeNull();
+  const enabled = mountField();
+  enabled.start(1);
+  move(0);
+  move(3);
+  expect(enabled.values).toEqual([]);
+  expect(enabled.commits).not.toHaveBeenCalled();
+});
+
+function mountField(disabled = false) {
+  const node = document.createElement("div");
+  document.body.append(node);
+  const root = createRoot(node);
+  const values: number[] = [];
+  const commits = vi.fn();
+  act(() => root.render(<NumberField icon="X" value="10" disabled={disabled} onScrub={(value) => values.push(value)} onScrubEnd={commits} onChange={() => {}} />));
+  mounted.push({ root, node });
+  return { root, values, commits, start(button = 0) {
+    act(() => node.querySelector("button")!.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, button })));
+  } };
+}
+
+function move(movementX: number) {
+  const event = new MouseEvent("pointermove", { bubbles: true });
+  Object.defineProperty(event, "movementX", { value: movementX });
+  act(() => document.dispatchEvent(event));
+}
+
+function restoreDescriptor(target: object, name: string, descriptor: PropertyDescriptor | undefined) {
+  if (descriptor) Object.defineProperty(target, name, descriptor);
+  else Reflect.deleteProperty(target, name);
+}
diff --git a/frontend/libs/components/video-editor/src/lib/components/ui/number-field.tsx b/frontend/libs/components/video-editor/src/lib/components/ui/number-field.tsx
--- a/frontend/libs/components/video-editor/src/lib/components/ui/number-field.tsx
+++ b/frontend/libs/components/video-editor/src/lib/components/ui/number-field.tsx
@@ -2,7 +2,7 @@
 
 import { cn } from "../../utils/ui";
 import { clamp } from "../../utils/math";
-import { useRef, useState, useLayoutEffect, type ComponentProps } from "react";
+import { useRef, useState, useEffect, useLayoutEffect, type ComponentProps } from "react";
 import { useFocusLock } from "../../hooks/use-focus-lock";
 import { Button } from "./button";
 import { HugeiconsIcon } from "@hugeicons/react";
@@ -139,10 +139,15 @@ function NumberField({
   const ghostRef = useRef<HTMLSpanElement>(null);
   const startValueRef = useRef(0);
   const cumulativeDeltaRef = useRef(0);
+  const cleanupScrubRef = useRef<((commit: boolean) => void) | null>(null);
   const [isInputFocused, setIsInputFocused] = useState(false);
   const [suffixLeft, setSuffixLeft] = useState(0);
   const ghostValue = Array.isArray(value) ? value.join(", ") : String(value ?? "");
 
+  useEffect(() => () => {
+    cleanupScrubRef.current?.(false);
+  }, []);
+
   useLayoutEffect(() => {
     if (!suffix) {
       setSuffixLeft(0);
@@ -166,11 +171,14 @@ function NumberField({
 
   const handleIconPointerDown = (event: React.PointerEvent) => {
     if (!onScrub || disabled || event.button !== 0) return;
+    cleanupScrubRef.current?.(false);
+    const scrubTarget = iconRef.current;
+    if (!scrubTarget) return;
     const parsed = parseFloat(String(value ?? "0"));
     startValueRef.current = Number.isNaN(parsed) ? 0 : parsed;
     cumulativeDeltaRef.current = 0;
     let hasReceivedFirstMove = false;
-    iconRef.current?.requestPointerLock();
+    scrubTarget.requestPointerLock();
 
     const handlePointerMove = (moveEvent: PointerEvent) => {
       // first movementX after pointer lock often contains a bogus warp delta
@@ -192,15 +200,28 @@ function NumberField({
       onScrub(newValue);
     };
 
-    const handlePointerUp = () => {
+    const cleanup = (commit: boolean) => {
+      if (cleanupScrubRef.current !== cleanup) return;
+      cleanupScrubRef.current = null;
       document.removeEventListener("pointermove", handlePointerMove);
       document.removeEventListener("pointerup", handlePointerUp);
-      document.exitPointerLock();
-      onScrubEnd?.();
+      document.removeEventListener("pointerlockchange", handlePointerLockChange);
+      window.removeEventListener("blur", handlePointerUp);
+      if (document.pointerLockElement === scrubTarget) {
+        document.exitPointerLock();
+      }
+      if (commit) onScrubEnd?.();
+    };
+    const handlePointerUp = () => cleanup(true);
+    const handlePointerLockChange = () => {
+      if (document.pointerLockElement !== scrubTarget) cleanup(true);
     };
 
+    cleanupScrubRef.current = cleanup;
     document.addEventListener("pointermove", handlePointerMove);
     document.addEventListener("pointerup", handlePointerUp);
+    document.addEventListener("pointerlockchange", handlePointerLockChange);
+    window.addEventListener("blur", handlePointerUp);
   };
 
   const canScrub = Boolean(icon && onScrub);
