Skip to content

feat: cursor follow crop with text cursor focus mode#640

Open
walters954 wants to merge 3 commits into
webadderallorg:mainfrom
walters954:feature/cursor-follow-crop
Open

feat: cursor follow crop with text cursor focus mode#640
walters954 wants to merge 3 commits into
webadderallorg:mainfrom
walters954:feature/cursor-follow-crop

Conversation

@walters954

@walters954 walters954 commented Jun 1, 2026

Copy link
Copy Markdown

When recording screen captures with a crop applied (e.g. 1080p output from a 1440p source), the viewport tracks the mouse cursor — which works great when you're moving the mouse, but causes the frame to drift away from the typing area whenever you stop to type. This PR adds cursor-follow crop: a per-frame viewport pan that keeps the cursor inside a configurable safe zone, plus an opt-in text cursor focus mode that locks the viewport to the typing area when the I-beam is active and the mouse is stationary.

What's in the PR

Core algorithm (videoPlayback/cursorFollowCrop.ts) — stateful per-frame computation that reads cursor telemetry, interpolates position, and eases the viewport top-left so the cursor stays inside the safe-zone inset. No dependencies on anything outside the existing telemetry pipeline.

Text cursor focus — optional mode that detects mouse velocity over a 400ms look-back window and the cursorType field already present in cursor.json telemetry. When the mouse has been still for 700ms and the cursor type is "text", the viewport locks to the typing area with a high smoothness floor (0.92). Mouse movement immediately snaps back to tracking mode. Debounced to avoid jarring transitions.

UI — a "Track cursor" toggle in the crop panel with safe zone and smoothness sliders, plus the text cursor focus checkbox. All settings persist with the project.

Export — wired into FrameRenderer/modernFrameRenderer so the same crop behavior applies during export.

Tests — 9 unit tests covering: edge clamping, safe zone hold, scrub reinit, telemetry fallback, mouse/text mode switching, and mode reset on scrub backwards.

Files changed

  • types.tsCursorFollowCropSettings, DEFAULT_CURSOR_FOLLOW_CROP
  • videoPlayback/cursorFollowCrop.ts — new file, core algorithm
  • videoPlayback/cursorFollowCrop.test.ts — new file, tests
  • CropControl.tsx — crop panel UI
  • VideoPlayback.tsx — preview integration + dep array update
  • projectPersistence.ts — load/save normalization
  • frameRenderer.ts, modernFrameRenderer.ts — export pipeline
  • videoExporter.ts, modernVideoExporter.ts, gifExporter.ts — config threading

Testing

Enable Track cursor on a 1440p recording cropped to 1080p. The viewport should pan to keep the cursor in frame during playback and export. With Text cursor focus on, scrubbing to a typing section should show the viewport staying stable on the text field; moving the mouse should immediately resume tracking.

Summary by CodeRabbit

Release Notes

New Features

  • Cursor-follow crop mode: Automatically pans and resizes the crop area to track cursor position, with safe zones, smoothing, preview modes, and optional “text cursor” zoom behavior.
  • Exports and project save/load now preserve the cursor-follow crop setting.

Bug Fixes

  • Improved responsiveness when scrubbing/pausing so cursor-follow tracking resets correctly.

New Features

  • Support for M4A audio files.

Chores / Improvements

  • Faster, safer audio waveform generation with size limits and reduced memory usage.

@github-actions github-actions Bot added the Slop label Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a cursor-follow crop mode for video editors: type definitions and defaults, per-frame crop computation from cursor telemetry with text-zoom engagement layer, CropControl UI controls and updated drag behavior, VideoPlayback ticker integration, VideoEditor state threading and export wiring, project persistence and normalization, export renderer application in both standard and modern pipelines, exporter config updates, comprehensive tests, and related waveform decode and media content-type improvements.

Changes

Cursor-follow crop feature

Layer / File(s) Summary
Cursor-follow crop types and configuration
src/components/video-editor/types.ts
Defines CursorFollowCropPreviewMode union ("source" | "output"), CursorFollowCropSettings interface with required tuning fields and optional text-zoom controls, and DEFAULT_CURSOR_FOLLOW_CROP constant providing initial defaults.
Cursor-follow crop computation engine and tests
src/components/video-editor/videoPlayback/cursorFollowCrop.ts, src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
State tracking, initialization/reset, telemetry detection helpers (detectMouseActivity, isTextCursorActive), target position computation (computeTargetPosition, computeCenteredPosition), response-factor interpolation, and main per-frame computeCursorFollowCrop algorithm managing focus mode and safe-zone panning; tested for edge clamping, safe-zone behavior, time scrubbing, and focus-mode transitions.
Cursor text-zoom engagement state and tests
src/components/video-editor/videoPlayback/cursorTextZoom.ts, src/components/video-editor/videoPlayback/cursorTextZoom.test.ts
Depth clamping, focus coordinate clamping, telemetry-based last-movement detection, state tracking, and computeCursorTextZoom that toggles active/inactive based on stillness debounce, text-cursor detection, and mouse-activity priority; tested for debounce behavior, cursor-type filtering, movement disengage, custom depth, and scrub transitions.
CropControl UI cursor-follow controls and drag behavior
src/components/video-editor/CropControl.tsx
Extends props with cursor-follow settings, telemetry, and playback time; adds output resolution presets with active matching; introduces follow-mode state and effects that reset/recompute effective crop from telemetry; updates canvas drawing to show full or cropped "output" region; refactors pointer drag to resize-only (no reposition) when follow is enabled or to update x/y with bounds when disabled; adds cursor-follow UI controls (toggle, presets, safe zone, smoothness, text cursor focus, preview mode); gates overlay/handle rendering based on computed crop pixel coordinates.
VideoEditor state and export config threading
src/components/video-editor/VideoEditor.tsx
Adds cursorFollowCrop React state initialized from defaults; includes it in persisted editor state serialization and restoration; threads the setting through FrameRenderer (thumbnails), VideoPlayback (preview), CropControl (UI), GIF/MP4 exporters; adds to export hook dependency list to ensure exports always use the latest setting.
VideoPlayback preview ticker integration
src/components/video-editor/VideoPlayback.tsx
Adds cursorFollowCrop prop; introduces refs for cursor-follow crop settings/state and base/effective crop regions; syncs settings and resets state on changes; updates Pixi ticker to compute and apply effectiveCropRegion per frame when enabled (sprite offset + mask source-crop update), or restore base crop/offset when disabled; adds cursor text-zoom layer that computes zoom target when enabled and no explicit zoom is active, or resets when disabled.
Project persistence and normalization
src/components/video-editor/projectPersistence.ts
Extends ProjectEditorState with cursorFollowCrop field; normalizes persisted cursor-follow crop by preserving/defaulting previewMode, clamping enabled, safeZoneRatio, smoothness, trackTextCursor, and conditionally including textZoomEnabled/textZoomDepth (clamped to [1, 4]).
Export renderer cursor-follow crop application
src/lib/exporter/frameRenderer.ts, src/lib/exporter/modernFrameRenderer.ts
Both renderers add cursorFollowCropState initialization and applyCursorFollowCrop(timeMs, layoutCache) that computes effective crop and updates sprite position/mask source-crop, or restores base crop when disabled; called in temporal snapshot and regular scene sampling before cursor overlay updates. Both add text-zoom layer in updateAnimationState that applies zoom scale/focus when enabled and no explicit zoom is active, or resets when disabled.
Exporter config and renderer initialization
src/lib/exporter/videoExporter.ts, src/lib/exporter/gifExporter.ts, src/lib/exporter/modernVideoExporter.ts
Adds cursorFollowCrop?: CursorFollowCropSettings to exporter config types and FrameRenderConfig; wires config through constructors to renderer initialization; adds native static-layout skip reason cursor-follow-crop-dynamic-layout when cursor-follow crop is enabled.
Waveform decoding and media content-type support
src/components/video-editor/audio/waveform/WaveformGenerator.ts, electron/mediaTypes.ts
WaveformGenerator replaces AudioContext-based decode with OfflineAudioContext at coarse sample rate to reduce memory; adds MAX_DECODE_BYTES threshold and URL size probing to prevent OOM. mediaTypes.ts adds .m4a extension mapping to audio/mp4 for browser content-type support.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • webadderallorg/Recordly#465: Modifies cursor overlay rendering order and inputs in frame renderers—directly related to the cursor-follow crop feature's overlay/rendering pipeline changes.
  • webadderallorg/Recordly#416: Changes cursor telemetry capture and persistence APIs on the electron side that produce the CursorTelemetryPoint[] samples consumed by the cursor-follow crop and text-zoom computations.

"I hopped through frames to follow your hand,
Safe zones keep stillness, panning calm and grand,
Typing stays steady while I watch and stand,
Exports carry the follow across the land." 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature added in this PR: cursor-follow crop functionality with text cursor focus mode support.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering motivation, implementation details, files changed, and testing instructions. It exceeds the template requirements significantly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

Adds a "Track cursor" crop mode that pans the viewport to keep the mouse
cursor inside a configurable safe zone during playback and export. A new
"Text cursor focus" toggle locks the viewport over the typing area when
the I-beam cursor is active and the mouse is stationary, then smoothly
switches back to mouse tracking once movement is detected. Mouse always
wins; a 700ms debounce prevents jarring transitions between modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@walters954 walters954 force-pushed the feature/cursor-follow-crop branch from 2bfe0d4 to 75ee15c Compare June 1, 2026 03:44
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

1 similar comment
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/video-editor/VideoEditor.tsx (1)

1078-1104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add cursorFollowCrop to the thumbnail callback deps.

captureProjectThumbnail() now closes over cursorFollowCrop here, but the useCallback dependency list below doesn't include it. Saving right after changing Track cursor can therefore bake the previous cursor-follow config into the project thumbnail.

💡 Minimal fix
 	}, [
 		annotationRegions,
 		autoCaptionSettings,
 		autoCaptions,
 		backgroundBlur,
 		borderRadius,
 		connectZooms,
 		connectedZoomDurationMs,
 		connectedZoomEasing,
 		connectedZoomGapMs,
 		cropRegion,
+		cursorFollowCrop,
 		currentTime,
 		cursorClickBounce,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1078 - 1104, The
captureProjectThumbnail callback in VideoEditor closes over cursorFollowCrop but
its useCallback dependency list is missing that variable; update the useCallback
that defines captureProjectThumbnail to include cursorFollowCrop (alongside the
other deps such as wallpaper, zoomRegions, targetWidth/Height, etc.) so the
thumbnail logic uses the current cursor-follow setting when saved.
src/lib/exporter/gifExporter.ts (1)

143-201: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward cursorFollowCrop into the GIF frame renderer config.

GifExporterConfig accepts the new setting, but buildGifFrameRendererConfig() drops it. As written, GIF exports won't apply Track cursor even though preview and MP4 exports do.

Suggested fix
 		borderRadius: config.borderRadius,
 		padding: config.padding,
 		cropRegion: config.cropRegion,
+		cursorFollowCrop: config.cursorFollowCrop,
 		webcam: config.webcam,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/exporter/gifExporter.ts` around lines 143 - 201, The
buildGifFrameRendererConfig() return object is missing the cursorFollowCrop
option from GifExporterConfig, so GIFs ignore Track cursor; update the object
returned by buildGifFrameRendererConfig to include cursorFollowCrop:
config.cursorFollowCrop (ensuring the key matches the renderer's expected name)
so the GIF frame renderer receives and applies the setting.
🧹 Nitpick comments (1)
src/components/video-editor/VideoPlayback.tsx (1)

1551-1554: 💤 Low value

Consider splitting ref update and state reset into separate effects.

The current effect updates cursorFollowCropRef.current and resets state, but the dependency array only includes specific properties. If cursorFollowCrop object reference changes but these properties remain the same, the ref won't be updated. While this works functionally (since the properties are identical), it could be clearer to separate concerns:

+useEffect(() => {
+  cursorFollowCropRef.current = cursorFollowCrop;
+}, [cursorFollowCrop]);
+
 useEffect(() => {
-  cursorFollowCropRef.current = cursorFollowCrop;
   resetCursorFollowCropState(cursorFollowCropStateRef.current);
 }, [cursorFollowCrop?.enabled, cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness, cursorFollowCrop?.trackTextCursor]);

This makes the intent clearer: always keep the ref synced, but only reset state when computation-affecting properties change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoPlayback.tsx` around lines 1551 - 1554,
Split the current useEffect into two: one effect that always syncs the ref by
assigning cursorFollowCropRef.current = cursorFollowCrop and depends on the
cursorFollowCrop object reference (so it runs whenever the object changes), and
a separate effect that calls
resetCursorFollowCropState(cursorFollowCropStateRef.current) with dependencies
only on the computation-affecting properties (cursorFollowCrop?.enabled,
cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness,
cursorFollowCrop?.trackTextCursor); keep references to
resetCursorFollowCropState, cursorFollowCropRef, and cursorFollowCropStateRef
as-is to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/CropControl.tsx`:
- Around line 115-118: The preview is being stretched because
canvas.width/canvas.height are always set to sourceWidth/sourceHeight; when in
output mode use the cropped region's dimensions instead of the full source to
preserve aspect ratio—compute the targetWidth/targetHeight from the current crop
rectangle (e.g., cropRect.width/cropRect.height or cropWidth/cropHeight) or
scale them to the configured output resolution, and set canvas.width and
canvas.height to those values before drawing; update the same logic where
sourceWidth/sourceHeight are used (the blocks around canvas.width/canvas.height
at the given ranges) so the canvas matches the crop/output aspect rather than
the full video.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 6357-6358: The crop modal is being passed raw cursorTelemetry
which can differ from the normalized/loop-aware path used by VideoPlayback and
export; change the prop passed to the crop editor from cursorTelemetry to the
already-computed effectiveCursorTelemetry (the same value used by
VideoPlayback/export) so the crop preview uses the identical telemetry variant;
locate the prop usage in VideoEditor (prop name cursorTelemetry/currentTimeMs)
and replace it to supply effectiveCursorTelemetry (or call the helper that
computes it) so preview/export paths match.
- Around line 6355-6356: The cancel path only restores cropRegion but not the
mutable cursor-follow state, so when the crop modal is opened you should
snapshot the current cursorFollowCrop (e.g., prevCursorFollowCrop) and any
related fields (safe zone, smoothness) and then in handleCancelCropEditor()
restore cursorFollowCrop via setCursorFollowCrop(prevCursorFollowCrop) (and
clear the snapshot after). Add the snapshot creation where the editor opens and
use the snapshot in handleCancelCropEditor to roll back cursorFollowCrop
changes.

In `@src/lib/exporter/modernFrameRenderer.ts`:
- Line 2984: The call to applyCursorFollowCrop(timeMs, layoutCache) uses
source/output timeMs when sampling cursor-follow crop but cursor sampling must
use the cursor timeline; update applyCursorFollowCrop (and any callers at the
other site) to accept and pass the timeline-aligned cursor time (cursorTimeMs)
or convert timeMs to cursorTimeMs via the same remapping used by
cursorOverlay.update before calling computeCursorFollowCrop so the crop sampling
uses cursorTimeMs (ensure references to applyCursorFollowCrop,
computeCursorFollowCrop, and cursorOverlay.update are updated accordingly).

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1078-1104: The captureProjectThumbnail callback in VideoEditor
closes over cursorFollowCrop but its useCallback dependency list is missing that
variable; update the useCallback that defines captureProjectThumbnail to include
cursorFollowCrop (alongside the other deps such as wallpaper, zoomRegions,
targetWidth/Height, etc.) so the thumbnail logic uses the current cursor-follow
setting when saved.

In `@src/lib/exporter/gifExporter.ts`:
- Around line 143-201: The buildGifFrameRendererConfig() return object is
missing the cursorFollowCrop option from GifExporterConfig, so GIFs ignore Track
cursor; update the object returned by buildGifFrameRendererConfig to include
cursorFollowCrop: config.cursorFollowCrop (ensuring the key matches the
renderer's expected name) so the GIF frame renderer receives and applies the
setting.

---

Nitpick comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1551-1554: Split the current useEffect into two: one effect that
always syncs the ref by assigning cursorFollowCropRef.current = cursorFollowCrop
and depends on the cursorFollowCrop object reference (so it runs whenever the
object changes), and a separate effect that calls
resetCursorFollowCropState(cursorFollowCropStateRef.current) with dependencies
only on the computation-affecting properties (cursorFollowCrop?.enabled,
cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness,
cursorFollowCrop?.trackTextCursor); keep references to
resetCursorFollowCropState, cursorFollowCropRef, and cursorFollowCropStateRef
as-is to locate the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 735219a8-1b54-4121-be56-645a82bfc981

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9912b and 75ee15c.

📒 Files selected for processing (12)
  • src/components/video-editor/CropControl.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/modernFrameRenderer.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts

Comment on lines +115 to +118
const sourceWidth = videoElement.videoWidth || 1920;
const sourceHeight = videoElement.videoHeight || 1080;
canvas.width = sourceWidth;
canvas.height = sourceHeight;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't stretch the output preview to the source aspect ratio.

In output mode the cropped region is still rendered into a full-source canvas and wrapped in a container sized from the full video. Any crop whose aspect differs from the source gets geometrically distorted, so the new Output preview can show the wrong framing.

Also applies to: 130-137, 271-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/CropControl.tsx` around lines 115 - 118, The
preview is being stretched because canvas.width/canvas.height are always set to
sourceWidth/sourceHeight; when in output mode use the cropped region's
dimensions instead of the full source to preserve aspect ratio—compute the
targetWidth/targetHeight from the current crop rectangle (e.g.,
cropRect.width/cropRect.height or cropWidth/cropHeight) or scale them to the
configured output resolution, and set canvas.width and canvas.height to those
values before drawing; update the same logic where sourceWidth/sourceHeight are
used (the blocks around canvas.width/canvas.height at the given ranges) so the
canvas matches the crop/output aspect rather than the full video.

Comment on lines +6355 to +6356
cursorFollow={cursorFollowCrop}
onCursorFollowChange={setCursorFollowCrop}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel should roll back cursor-follow edits too.

These props let the crop modal mutate cursorFollowCrop immediately, but handleCancelCropEditor() only restores cropRegion. If the user toggles Track cursor or changes safe zone/smoothness and then cancels, those edits still stick.

💡 Minimal fix
+	const cursorFollowSnapshotRef = useRef<CursorFollowCropSettings | null>(null);
+
 	const handleOpenCropEditor = useCallback(() => {
 		cropSnapshotRef.current = { ...cropRegion };
+		cursorFollowSnapshotRef.current = { ...cursorFollowCrop };
 		setShowCropModal(true);
-	}, [cropRegion]);
+	}, [cropRegion, cursorFollowCrop]);

 	const handleCancelCropEditor = useCallback(() => {
 		if (cropSnapshotRef.current) {
 			setCropRegion(cropSnapshotRef.current);
 		}
+		if (cursorFollowSnapshotRef.current) {
+			setCursorFollowCrop(cursorFollowSnapshotRef.current);
+		}
 		setShowCropModal(false);
-	}, []);
+	}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 6355 - 6356, The
cancel path only restores cropRegion but not the mutable cursor-follow state, so
when the crop modal is opened you should snapshot the current cursorFollowCrop
(e.g., prevCursorFollowCrop) and any related fields (safe zone, smoothness) and
then in handleCancelCropEditor() restore cursorFollowCrop via
setCursorFollowCrop(prevCursorFollowCrop) (and clear the snapshot after). Add
the snapshot creation where the editor opens and use the snapshot in
handleCancelCropEditor to roll back cursorFollowCrop changes.

Comment on lines +6357 to +6358
cursorTelemetry={cursorTelemetry}
currentTimeMs={currentTime * 1000}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same telemetry variant as preview/export.

The crop modal gets raw cursorTelemetry, while VideoPlayback and both export paths use effectiveCursorTelemetry after normalization/loop handling. With looped cursor playback or timeline-adjusted telemetry, the crop editor can preview a different camera path than the actual render.

💡 Minimal fix
-							cursorTelemetry={cursorTelemetry}
+							cursorTelemetry={effectiveCursorTelemetry}
 							currentTimeMs={currentTime * 1000}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cursorTelemetry={cursorTelemetry}
currentTimeMs={currentTime * 1000}
cursorTelemetry={effectiveCursorTelemetry}
currentTimeMs={currentTime * 1000}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 6357 - 6358, The
crop modal is being passed raw cursorTelemetry which can differ from the
normalized/loop-aware path used by VideoPlayback and export; change the prop
passed to the crop editor from cursorTelemetry to the already-computed
effectiveCursorTelemetry (the same value used by VideoPlayback/export) so the
crop preview uses the identical telemetry variant; locate the prop usage in
VideoEditor (prop name cursorTelemetry/currentTimeMs) and replace it to supply
effectiveCursorTelemetry (or call the helper that computes it) so preview/export
paths match.

const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;

this.applyCursorFollowCrop(timeMs, layoutCache);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the cursor timeline when sampling cursor-follow crop.

These call sites pass source-media time into computeCursorFollowCrop, but the crop logic is driven by cursor telemetry. When trims or speed regions remap output time, the crop will follow the wrong cursor sample while cursorOverlay.update(...) still uses cursorTimeMs, so exported framing diverges from the visible cursor.

Suggested fix
-		this.applyCursorFollowCrop(timeMs, layoutCache);
+		this.applyCursorFollowCrop(cursorTimeMs, layoutCache);

Also applies to: 3235-3235

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/exporter/modernFrameRenderer.ts` at line 2984, The call to
applyCursorFollowCrop(timeMs, layoutCache) uses source/output timeMs when
sampling cursor-follow crop but cursor sampling must use the cursor timeline;
update applyCursorFollowCrop (and any callers at the other site) to accept and
pass the timeline-aligned cursor time (cursorTimeMs) or convert timeMs to
cursorTimeMs via the same remapping used by cursorOverlay.update before calling
computeCursorFollowCrop so the crop sampling uses cursorTimeMs (ensure
references to applyCursorFollowCrop, computeCursorFollowCrop, and
cursorOverlay.update are updated accordingly).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

1175-1222: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing cursorFollowCrop in dependency array.

The captureProjectThumbnail callback uses cursorFollowCrop on line 1073 but does not include it in its dependency array. This can cause thumbnails to be generated with stale cursor-follow-crop settings if the setting changes after the callback is created.

🔧 Proposed fix

Add cursorFollowCrop to the dependency array:

 	}, [
 		annotationRegions,
 		autoCaptionSettings,
 		autoCaptions,
 		backgroundBlur,
 		borderRadius,
 		connectZooms,
 		connectedZoomDurationMs,
 		connectedZoomEasing,
 		connectedZoomGapMs,
 		cropRegion,
+		cursorFollowCrop,
 		currentTime,
 		cursorClickBounce,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1175 - 1222, The
captureProjectThumbnail callback uses cursorFollowCrop but the effect's
dependency array (the useCallback/useEffect that defines
captureProjectThumbnail) is missing cursorFollowCrop, causing stale values;
update the dependency array that currently lists annotationRegions,
autoCaptionSettings, ..., zoomClassicMode to include cursorFollowCrop so
captureProjectThumbnail re-creates when cursorFollowCrop changes (look for the
captureProjectThumbnail function reference and the large dependency array near
it).
🧹 Nitpick comments (1)
src/components/video-editor/CropControl.tsx (1)

318-318: 💤 Low value

Redundant conditional expression.

(!followEnabled ? true : true) always evaluates to true, so this line simplifies to !showOutputMode.

♻️ Suggested simplification
-	const showHandles = !showOutputMode && (!followEnabled ? true : true);
+	const showHandles = !showOutputMode;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/CropControl.tsx` at line 318, The assignment to
showHandles in CropControl.tsx uses a redundant ternary "(!followEnabled ? true
: true)" which always evaluates to true; replace the entire expression with a
simplified value by setting showHandles to "!showOutputMode" (remove the
unnecessary ternary and redundant checks around followEnabled) so the variable
reflects only the showOutputMode condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 2135-2137: The code currently only assigns to
baseMaskRef.current.sourceCrop when it already exists, so when cursor-follow
starts enabled that field may be undefined and downstream rendering misses the
effective crop; update the logic around baseMaskRef (referencing
baseMaskRef.current, sourceCrop, and effectiveCrop) to always initialize
sourceCrop to the effectiveCrop (e.g., set baseMaskRef.current.sourceCrop = {
...effectiveCrop } unconditionally or ensure initialization before cursor-follow
usage), ensuring the property exists whenever cursor-follow or rendering logic
reads it; keep the assignment in the same function where effectiveCrop is
computed so you don't change call sites.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1175-1222: The captureProjectThumbnail callback uses
cursorFollowCrop but the effect's dependency array (the useCallback/useEffect
that defines captureProjectThumbnail) is missing cursorFollowCrop, causing stale
values; update the dependency array that currently lists annotationRegions,
autoCaptionSettings, ..., zoomClassicMode to include cursorFollowCrop so
captureProjectThumbnail re-creates when cursorFollowCrop changes (look for the
captureProjectThumbnail function reference and the large dependency array near
it).

---

Nitpick comments:
In `@src/components/video-editor/CropControl.tsx`:
- Line 318: The assignment to showHandles in CropControl.tsx uses a redundant
ternary "(!followEnabled ? true : true)" which always evaluates to true; replace
the entire expression with a simplified value by setting showHandles to
"!showOutputMode" (remove the unnecessary ternary and redundant checks around
followEnabled) so the variable reflects only the showOutputMode condition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 083ab8d0-6c60-4020-bfcf-827871a42fad

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9912b and 2bfe0d4.

📒 Files selected for processing (12)
  • src/components/video-editor/CropControl.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/modernFrameRenderer.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts

Comment on lines +2135 to +2137
if (baseMaskRef.current.sourceCrop) {
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

sourceCrop may never be initialized when cursor-follow is enabled.

The code only mutates baseMaskRef.current.sourceCrop if it already exists, but nothing initializes it. If cursor-follow is enabled from the start, downstream rendering logic expecting sourceCrop won't receive the effective crop coordinates.

Consider initializing sourceCrop unconditionally:

🐛 Proposed fix
-				if (baseMaskRef.current.sourceCrop) {
-					baseMaskRef.current.sourceCrop = { ...effectiveCrop };
-				}
+				baseMaskRef.current.sourceCrop = { ...effectiveCrop };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (baseMaskRef.current.sourceCrop) {
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
}
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoPlayback.tsx` around lines 2135 - 2137, The
code currently only assigns to baseMaskRef.current.sourceCrop when it already
exists, so when cursor-follow starts enabled that field may be undefined and
downstream rendering misses the effective crop; update the logic around
baseMaskRef (referencing baseMaskRef.current, sourceCrop, and effectiveCrop) to
always initialize sourceCrop to the effectiveCrop (e.g., set
baseMaskRef.current.sourceCrop = { ...effectiveCrop } unconditionally or ensure
initialization before cursor-follow usage), ensuring the property exists
whenever cursor-follow or rendering logic reads it; keep the assignment in the
same function where effectiveCrop is computed so you don't change call sites.

walters954 and others added 2 commits June 11, 2026 22:34
…nd file size limit

Added support for decoding audio waveforms at a lower sample rate to optimize memory usage, preventing out-of-memory errors for large files. Introduced a maximum file size limit for in-memory audio decoding to ensure stability during processing. Additionally, registered a new audio content type for .m4a files.
Text-cursor focus (cursorFollowCrop) now actively eases the viewport to
center the I-beam (the typing spot) while typing instead of merely
freezing it, snapping back to safe-zone mouse tracking the instant the
mouse moves.

Adds an independent, toggleable "Text zoom" layer that punches the
camera in on the typing area when sustained typing is detected and eases
back out on mouse movement. Explicit zoom regions always win over it. The
layer seeds its still-detection from the telemetry window so it engages
when paused/scrubbing, not just during playback, and is applied in both
export paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/exporter/modernFrameRenderer.ts (1)

3002-3002: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sample text zoom on the cursor timeline.

computeCursorTextZoom is driven by cursorTelemetry, but Line 3831 uses timeMs while the export path already has cursorTimeMs. With trims or speed regions, text zoom can focus/activate on the wrong telemetry sample even when the rendered cursor is correct.

Proposed fix
-		this.updateAnimationState(timeMs);
+		this.updateAnimationState(timeMs, cursorTimeMs);
-		this.updateAnimationState(timeMs);
+		this.updateAnimationState(timeMs, cursorTimeMs);
-	private updateAnimationState(timeMs: number): number {
+	private updateAnimationState(timeMs: number, cursorTimeMs = timeMs): number {
 			const textZoom = computeCursorTextZoom(
 				this.cursorTextZoomState,
 				this.config.cursorTelemetry,
-				timeMs,
+				cursorTimeMs,
 				this.config.cursorFollowCrop,
 			);

Also applies to: 3253-3253, 3736-3736, 3831-3831

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/exporter/modernFrameRenderer.ts` at line 3002, The
`computeCursorTextZoom` function uses cursor telemetry which must be sampled
using `cursorTimeMs` rather than `timeMs` to correctly handle trims and speed
regions in the export path. In src/lib/exporter/modernFrameRenderer.ts, replace
all instances where cursor-related operations (particularly calls to
`computeCursorTextZoom`) are parameterized with `timeMs` and replace them with
`cursorTimeMs` instead. This affects the following locations: line 3253, line
3736, and line 3831. At each site, identify the call to `computeCursorTextZoom`
or similar cursor telemetry sampling operation and change the time parameter
passed from `timeMs` to `cursorTimeMs` to ensure the correct telemetry sample is
selected regardless of trims or speed regions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/videoPlayback/cursorTextZoom.ts`:
- Around line 38-43: The clampDepth function incorrectly rejects a depth value
of 1 by treating it as invalid, but persistence accepts 1 as a valid value
(valid range is 1..4). In the clampDepth function, change the condition from
`depth <= 1` to `depth < 1` so that a persisted value of 1 is honored and
returned instead of being overridden with DEFAULT_TEXT_ZOOM_DEPTH_SCALE. This
allows valid saved values to be properly reproduced.

---

Outside diff comments:
In `@src/lib/exporter/modernFrameRenderer.ts`:
- Line 3002: The `computeCursorTextZoom` function uses cursor telemetry which
must be sampled using `cursorTimeMs` rather than `timeMs` to correctly handle
trims and speed regions in the export path. In
src/lib/exporter/modernFrameRenderer.ts, replace all instances where
cursor-related operations (particularly calls to `computeCursorTextZoom`) are
parameterized with `timeMs` and replace them with `cursorTimeMs` instead. This
affects the following locations: line 3253, line 3736, and line 3831. At each
site, identify the call to `computeCursorTextZoom` or similar cursor telemetry
sampling operation and change the time parameter passed from `timeMs` to
`cursorTimeMs` to ensure the correct telemetry sample is selected regardless of
trims or speed regions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64ec1d76-567d-4ee8-8873-f391a1a6613f

📥 Commits

Reviewing files that changed from the base of the PR and between 33a4677 and ceafc4d.

📒 Files selected for processing (10)
  • src/components/video-editor/CropControl.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/components/video-editor/videoPlayback/cursorTextZoom.test.ts
  • src/components/video-editor/videoPlayback/cursorTextZoom.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/modernFrameRenderer.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/components/video-editor/CropControl.tsx

Comment on lines +38 to +43
function clampDepth(depth: number | undefined): number {
if (!Number.isFinite(depth) || depth === undefined || depth <= 1) {
return DEFAULT_TEXT_ZOOM_DEPTH_SCALE;
}
return Math.min(4, depth);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor persisted textZoomDepth = 1 instead of overriding it.

At Line 39, depth <= 1 falls back to DEFAULT_TEXT_ZOOM_DEPTH_SCALE, but persistence accepts/clamps depth to 1..4. This makes a valid saved value (1) impossible to reproduce.

Suggested fix
function clampDepth(depth: number | undefined): number {
-	if (!Number.isFinite(depth) || depth === undefined || depth <= 1) {
+	if (!Number.isFinite(depth) || depth === undefined || depth < 1) {
		return DEFAULT_TEXT_ZOOM_DEPTH_SCALE;
	}
	return Math.min(4, depth);
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function clampDepth(depth: number | undefined): number {
if (!Number.isFinite(depth) || depth === undefined || depth <= 1) {
return DEFAULT_TEXT_ZOOM_DEPTH_SCALE;
}
return Math.min(4, depth);
}
function clampDepth(depth: number | undefined): number {
if (!Number.isFinite(depth) || depth === undefined || depth < 1) {
return DEFAULT_TEXT_ZOOM_DEPTH_SCALE;
}
return Math.min(4, depth);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/videoPlayback/cursorTextZoom.ts` around lines 38
- 43, The clampDepth function incorrectly rejects a depth value of 1 by treating
it as invalid, but persistence accepts 1 as a valid value (valid range is 1..4).
In the clampDepth function, change the condition from `depth <= 1` to `depth <
1` so that a persisted value of 1 is honored and returned instead of being
overridden with DEFAULT_TEXT_ZOOM_DEPTH_SCALE. This allows valid saved values to
be properly reproduced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants