Skip to content

feat/timeline-scrubbing : Mouse now scrubs timeline.#437

Merged
webadderall merged 5 commits intowebadderallorg:mainfrom
ExtraBinoss:feat/timeline-scrubbing
May 9, 2026
Merged

feat/timeline-scrubbing : Mouse now scrubs timeline.#437
webadderall merged 5 commits intowebadderallorg:mainfrom
ExtraBinoss:feat/timeline-scrubbing

Conversation

@ExtraBinoss
Copy link
Copy Markdown
Contributor

@ExtraBinoss ExtraBinoss commented May 7, 2026

Description

This PR introduces a scrubbing ability to the timeline. It means that if you are in the Timeline, you can drag around to search the perfect spot. If your video is playing, it stops. Sound is not played while scrubbing.

Keep in mind ;

  • Some video editors also play the sound while scrubbing, but I think it's out of scope of Recordly
  • Video is playing and you scrub == video stops. This is to be able to stop at a certain spot.

Type of Change

  • New Feature/ Enhancements

Related Issue(s)

None

Screenshots / Video

New Recordly :
https://github.com/user-attachments/assets/eb7a5581-ee65-4052-9781-8df6101bde46

Capcut behavior :
https://github.com/user-attachments/assets/a2e3b631-296e-4b6c-ad7a-557117a0e6cb

Testing Guide

  • You can test by clicking and dragging around in the timeline spot where the seconds are displayed. Outside of that scrubbing will not happen (to avoid interfering with clip dragging or other stuff).
  • You can resume the playback, and it will launch from that new point

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.

Summary by CodeRabbit

  • Improvements
    • Smoother, more responsive timeline scrubbing with drag-based seeking and improved cursor behavior.
    • Scrubbing now pauses playback when appropriate to preserve playback state during seeks.
    • Empty-space seeking starts on mouse-down (not click) with throttled updates and reliable cleanup of listeners.
    • Timeline items are marked to prevent accidental background selection during drags.

… intended from multiple video editor.

scrubbing stops the playhead. as per usual editors
Copilot AI review requested due to automatic review settings May 7, 2026 12:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4acda249-a5f9-4df3-97f7-32d2c402d1c4

📥 Commits

Reviewing files that changed from the base of the PR and between d72ccfe and 4b5f6f3.

📒 Files selected for processing (1)
  • src/components/video-editor/VideoEditor.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/video-editor/VideoEditor.tsx

📝 Walkthrough

Walkthrough

Timeline scrubbing now pauses playback and seeks via a unified handleSeek(time, {pause?}); timeline dragging and cursor dragging use rAF-throttled mouse tracking, TimelineAxis accepts an onMouseDown, background mousedown clears selection unless target is a timeline item, and items mark themselves with data-timeline-item.

Changes

Timeline Seeking with RequestAnimationFrame Throttling

Layer / File(s) Summary
Core Seek Behavior
src/components/video-editor/VideoEditor.tsx
handleSeek() replaced by handleSeek(time, options) that reads playback and video from videoPlaybackRef.current, optionally pauses playback when options.pause is true, and sets video.currentTime. handleTimelineSeek calls it with pause: true. TimelineEditor.onSeek now uses handleTimelineSeek.
Timeline Axis Interaction Contract
src/components/video-editor/timeline/TimelineEditor.tsx
TimelineAxis accepts an optional onMouseDown prop and applies it to the axis container to initiate seeks; cursor shows horizontal-resize affordance.
PlaybackCursor RAF Throttling
src/components/video-editor/timeline/TimelineEditor.tsx
PlaybackCursor refactored to throttle seek-drag mouse tracking using requestAnimationFrame with a cached lastMouseEvent and explicit cancelAnimationFrame cleanup.
Timeline Seeking State & Wiring
src/components/video-editor/timeline/TimelineEditor.tsx
Timeline introduces isSeeking state, adds handleTimelineMouseDown for initial seek and selection clear, and registers global mousemove/mouseup listeners (rAF-driven) during drag; top-level container uses background onMouseDown to clear selection unless target is [data-timeline-item].
Timeline Item DOM Marker
src/components/video-editor/timeline/Item.tsx
Timeline item root now includes data-timeline-item="true" to let background mousedowns ignore items when clearing selection and starting seeks.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TimelineAxis
  participant Timeline
  participant VideoEditor.handleSeek
  participant VideoElement
  User->>TimelineAxis: mousedown / drag
  TimelineAxis->>Timeline: onMouseDown -> handleTimelineMouseDown
  Timeline->>VideoEditor.handleSeek: compute time, handleSeek(time, {pause:true})
  VideoEditor.handleSeek->>VideoElement: pause() (if needed) then set currentTime
  Timeline->>Timeline: register window mousemove/mouseup (rAF) for continuous seeks
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 I hop along the timeline bright,
rAF keeps scrub motion light,
I pause the play and jump to frame,
Cleared selections, smooth seek game,
My whiskers mark each tiny time.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and concisely summarizes the main feature: mouse scrubbing capability for the timeline.
Description check ✅ Passed The PR description covers most required template sections: purpose, motivation, type of change, screenshots/video, and testing guide are all provided. Only the related issues and self-review sections are partially incomplete, but the checklist items indicate self-review was performed.
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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/timeline/TimelineEditor.tsx (1)

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

Flush the last drag position on mouseup before tearing down the rAF loop.

Both new seek loops cancel the pending animation frame during cleanup. If the user releases quickly, the last mousemove never reaches onSeek, so scrubbing can land behind the cursor on release.

Suggested fix pattern

Apply the same change to both handlers:

 const handleMouseUp = () => {
+	if (frameId !== null) {
+		cancelAnimationFrame(frameId);
+		frameId = null;
+	}
+	if (lastMouseEvent) {
+		updateSeek();
+	}
 	setIsDragging(false);
 	document.body.style.cursor = "";
 };

For the timeline-axis variant, mirror the same flush before setIsSeeking(false).

Also applies to: 723-763

🤖 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/timeline/TimelineEditor.tsx` around lines 326 -
379, On mouseup we must flush the pending drag position so the last mousemove is
applied before cancelling rAF; call updateSeek() (only if lastMouseEvent is set)
from handleMouseUp before clearing frameId/cancelling animation and before
calling setIsDragging(false) and resetting the cursor. Apply the same pattern to
the timeline-axis mouseup handler: invoke the corresponding flush function (the
same updateSeek or its axis-variant) before setIsSeeking(false) and before
cancelling any pending frame.
🤖 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/VideoEditor.tsx`:
- Around line 3277-3286: The handleSeek helper currently always pauses playback
(via videoPlaybackRef.current.pause()), causing non-scrub seeks to stop
playback; change handleSeek(time: number) to accept an optional flag like
pausePlayback: boolean = false, use that flag to conditionally call
playback.pause(), and keep setting currentTime via mapTimelineTimeToSourceTime;
update the timeline scrub caller to call handleSeek(time, true) while leaving
skip/jump button callers using the default (no flag) so they won't pause
playback.

---

Outside diff comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 326-379: On mouseup we must flush the pending drag position so the
last mousemove is applied before cancelling rAF; call updateSeek() (only if
lastMouseEvent is set) from handleMouseUp before clearing frameId/cancelling
animation and before calling setIsDragging(false) and resetting the cursor.
Apply the same pattern to the timeline-axis mouseup handler: invoke the
corresponding flush function (the same updateSeek or its axis-variant) before
setIsSeeking(false) and before cancelling any pending frame.
🪄 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: 86e68e5d-4f96-4f22-b6cc-557047335433

📥 Commits

Reviewing files that changed from the base of the PR and between 493c85f and a013e92.

📒 Files selected for processing (2)
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx

Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds timeline “scrubbing” so users can drag along the timeline axis to seek precisely, with playback stopping while scrubbing.

Changes:

  • Pause playback on seek so scrubbing doesn’t play audio and stops playback when currently playing (VideoEditor.tsx).
  • Throttle seek updates during drag using requestAnimationFrame to reduce seek spam (TimelineEditor.tsx).
  • Move seeking initiation to the timeline axis via onMouseDown + global mousemove tracking, and update cursor styling (TimelineEditor.tsx).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/components/video-editor/VideoEditor.tsx Pauses playback before applying seeks (used by timeline + other seek entrypoints).
src/components/video-editor/timeline/TimelineEditor.tsx Implements axis-based scrubbing with rAF throttling and window-level drag tracking; adjusts axis props/styling.

Comment thread src/components/video-editor/timeline/TimelineEditor.tsx Outdated
Comment thread src/components/video-editor/timeline/TimelineEditor.tsx
Comment thread src/components/video-editor/VideoEditor.tsx Outdated
@ExtraBinoss ExtraBinoss changed the title feat/timeline-scrubbing : Scrubs timeline. feat/timeline-scrubbing : Mouse now scrubs timeline. May 7, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/components/video-editor/timeline/TimelineEditor.tsx (1)

722-765: 💤 Low value

Clean drag-to-seek implementation.

The rAF throttling pattern is correctly implemented with proper cleanup.

One minor inconsistency: PlaybackCursor sets document.body.style.cursor = "ew-resize" during drag (line 374), but this effect doesn't. Since the TimelineAxis already has cursor-ew-resize class, this is likely fine, but if the user drags outside the axis bounds, the cursor might revert to default. Consider adding the same cursor override for visual consistency during drag.

🤖 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/timeline/TimelineEditor.tsx` around lines 722 -
765, When isSeeking is true the drag handler registered in useLayoutEffect
doesn't set the global cursor style, so dragging outside the axis can revert the
cursor; update the useLayoutEffect (the effect that registers
handleMouseMove/handleMouseUp) to set document.body.style.cursor = "ew-resize"
when isSeeking becomes true and restore the previous cursor on cleanup (reverse
the change in the returned cleanup function or store the original cursor),
ensuring you touch the same effect that uses setIsSeeking, localTimelineRef,
pixelsToValue, and onSeek so the cursor override is applied for the full drag
operation and removed when the effect unmounts.
🤖 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.

Nitpick comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 722-765: When isSeeking is true the drag handler registered in
useLayoutEffect doesn't set the global cursor style, so dragging outside the
axis can revert the cursor; update the useLayoutEffect (the effect that
registers handleMouseMove/handleMouseUp) to set document.body.style.cursor =
"ew-resize" when isSeeking becomes true and restore the previous cursor on
cleanup (reverse the change in the returned cleanup function or store the
original cursor), ensuring you touch the same effect that uses setIsSeeking,
localTimelineRef, pixelsToValue, and onSeek so the cursor override is applied
for the full drag operation and removed when the effect unmounts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c4983700-6e54-4d2a-8cfa-079830d17b77

📥 Commits

Reviewing files that changed from the base of the PR and between cfabfd7 and e32f504.

📒 Files selected for processing (2)
  • src/components/video-editor/timeline/Item.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/video-editor/timeline/Item.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/VideoEditor.tsx`:
- Around line 3424-3427: handleTimelineSeek is memoized with an empty deps array
and closes over handleSeek, causing a stale closure when
mapTimelineTimeToSourceTime (which depends on clipRegions) changes; update the
useCallback deps for handleTimelineSeek to include the changing references
(e.g., include handleSeek or mapTimelineTimeToSourceTime / clipRegions) so the
callback is recreated when timeline-to-source mapping changes, ensuring
handleTimelineSeek uses the latest mapping.
🪄 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: 28c99b3d-c0de-4535-9b33-7575b155c8a8

📥 Commits

Reviewing files that changed from the base of the PR and between e32f504 and d72ccfe.

📒 Files selected for processing (2)
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/video-editor/timeline/TimelineEditor.tsx

Comment thread src/components/video-editor/VideoEditor.tsx
@meiiie
Copy link
Copy Markdown
Collaborator

meiiie commented May 9, 2026

Triage pass on latest head 4b5f6f3: no active review threads, GitHub checks are green, and local tsc --noEmit, vite build --config vite.config.ts, and git diff --check passed. I did not find a blocker in this pass. Note for ordering: #448 still describes this scrubbing PR as the one to land first.

@webadderall webadderall merged commit 1d993b7 into webadderallorg:main May 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants