Skip to content

Add Timeline Snap Guides#515

Open
AbhinRustagi wants to merge 4 commits into
siddharthvaddem:mainfrom
AbhinRustagi:feature/timeline-handle-snap
Open

Add Timeline Snap Guides#515
AbhinRustagi wants to merge 4 commits into
siddharthvaddem:mainfrom
AbhinRustagi:feature/timeline-handle-snap

Conversation

@AbhinRustagi
Copy link
Copy Markdown
Contributor

@AbhinRustagi AbhinRustagi commented May 1, 2026

Pull Request Template

Description

Adds magnetic snap behavior to timeline handles. When dragging or resizing a trim, zoom, speed, annotation, or blur region, its edges snap to nearby region edges, the playhead, keyframes, and timeline bounds. A live amber guideline shows the active snap target during interaction; the drag tooltip previews the snapped values; on release, the region commits to the snapped position.

Motivation

Aligning two regions on the timeline is eyeball work right now

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

Closes #514 and partially #511

Screenshots / Video

Screenshot (if applicable):

snap.mp4

Testing

  1. Load a video into the editor.
  2. Add two zoom regions. Drag one near the edge of the other → its edge should snap to a perfect match, with an amber guideline appearing during the drag.
  3. Resize a zoom's left edge near the right edge of an adjacent trim → snap engages on the resized edge only; the other edge stays put.
  4. Drag any region near the playhead → snaps to the current time. (Guide line auto-hides when the snap target coincides with the
    playhead, so the green playback cursor isn't doubled.)
  5. Add a keyframe (default shortcut), then drag a region near it → snaps to the keyframe.
  6. Drag a region toward 0:00 or the video's end → snaps to the timeline bounds.
  7. Drag/resize an annotation or blur near another region's edge → snaps. Then drag two annotations onto each other → they still overlap freely (annotations/blurs are snap targets but not overlap constraints).
  8. Zoom the timeline in to the millisecond level and out to the full video → snap threshold scales with zoom (max(50ms, 1% of visible range)), so it stays usable at any zoom.

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Thank you for contributing!

Summary by CodeRabbit

  • New Features
    • Enhanced timeline snapping with a visible snap guide and on-screen snap tooltip during drag and resize.
    • Smarter constraint handling that differentiates hard constraints from soft snap targets to avoid unwanted clamping.
    • Timeline now respects keyframe positions and current playhead when snapping for more precise edits.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

Snap guides fire during timeline item drag/resize. TimelineEditor routes zoom/trim/speed spans as hard constraints and annotation/blur as soft snap targets, plus playhead and keyframes. TimelineWrapper snaps live spans, shows an amber guide at the snap point, applies snapping before overlap/min-duration checks, and hides overlays on end.

Changes

Snap Guide Timeline Interactions

Layer / File(s) Summary
Snapping Type Contract
src/components/video-editor/timeline/TimelineWrapper.tsx
TimelineWrapperProps adds optional softSnapSpans, currentTimeMs, and keyframeTimesMs.
Imports & Ref Utilities
src/components/video-editor/timeline/TimelineWrapper.tsx
Adds TimelineContext/useTimelineContext and React ref/imperative-handle utilities for SnapGuide.
SnapGuide Visual Component
src/components/video-editor/timeline/TimelineWrapper.tsx
New internal SnapGuide exposes imperative showAt(timeMs)/hide() and positions a vertical amber indicator using valueToPixels.
Props Destructuring & Defaults
src/components/video-editor/timeline/TimelineWrapper.tsx
TimelineWrapper accepts softSnapSpans and keyframeTimesMs (default []) and optionally uses currentTimeMs.
Snapping Infrastructure
src/components/video-editor/timeline/TimelineWrapper.tsx
Adds snapSpanToTargets, inferResizeMode, and updateSnapGuide to compute snapped spans from hard/soft spans, bounds, playhead, and keyframes.
Drag/Resize Move with Snapping
src/components/video-editor/timeline/TimelineWrapper.tsx
Move handlers compute raw span, clamp to bounds when applicable, snap via snapSpanToTargets, update snap guide, and render tooltip using snapped position and screen-X.
Drag/Resize End & Overlay Cleanup
src/components/video-editor/timeline/TimelineWrapper.tsx
End handlers apply snapping before min-duration and overlap/neighbor clamping; hideOverlays clears tooltip and snap guide; hook deps updated.
SnapGuide Rendering
src/components/video-editor/timeline/TimelineWrapper.tsx
Renders <SnapGuide ref={snapGuideRef} /> inside TimelineContext for pixel mapping.
Span & Keyframe Preparation
src/components/video-editor/timeline/TimelineEditor.tsx
allRegionSpans now includes only zoom/trim/speed; new softSnapSpans collects annotation/blur; keyframeTimesMs derived from keyframes; all passed to TimelineWrapper.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Poem

amber guide hums at the edge,
soft spans tug, hard spans hedge,
keyframes wink, the playhead stays,
drag finds rhythm in the haze.
🎬🧲

🚥 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 'Add Timeline Snap Guides' directly and clearly describes the main feature being added—magnetic snap behavior with visual guides for timeline handles.
Linked Issues check ✅ Passed The PR implements all coding objectives from #514: magnetic snap on drag/resize [handles], snap targets [edges/playhead/keyframes/bounds], visual snap guides, threshold scaling with zoom, and annotation/blur overlap preservation.
Out of Scope Changes check ✅ Passed All changes directly support the snap guides feature: TimelineEditor prepares span data for snapping; TimelineWrapper implements snap logic, SnapGuide rendering, and tooltip integration. No unrelated refactoring or scope creep detected.
Description check ✅ Passed PR description covers all required template sections with concrete details: clear feature description, solid motivation, proper issue links, testing steps, and checklist completion.

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


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.

@siddharthvaddem
Copy link
Copy Markdown
Owner

can you rework this based on new ui, add video of demo when you mark it ready for review?

@AbhinRustagi
Copy link
Copy Markdown
Contributor Author

AbhinRustagi commented May 11, 2026

@siddharthvaddem definitely, sounds good to me. Allow me some time (1/2 days) to get back to this and I'll have it ready for review

@AbhinRustagi
Copy link
Copy Markdown
Contributor Author

Also, taking this little chat to mention - amazing project! love it so far!

@AbhinRustagi AbhinRustagi changed the title feat: add timeline snap guides Add Timeline Snap Guides May 11, 2026
@AbhinRustagi AbhinRustagi marked this pull request as ready for review May 11, 2026 10:46
@AbhinRustagi AbhinRustagi force-pushed the feature/timeline-handle-snap branch from 9294b1f to d802473 Compare May 11, 2026 10:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9294b1f79d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/video-editor/timeline/TimelineWrapper.tsx Outdated
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 (2)
src/components/video-editor/timeline/TimelineWrapper.tsx (2)

435-468: 💤 Low value

nit: cleaner — extract the screenX calc

The event.activatorEvent && "clientX" in event.activatorEvent ? ... : undefined block is identical in onDragMove and onResizeMove. Tiny helper would tidy it up — totally skippable though, it's 5 lines.

♻️ Optional helper
+	const getScreenX = useCallback(
+		(event: { activatorEvent: Event | null; delta?: { x: number } }): number | undefined =>
+			event.activatorEvent && "clientX" in event.activatorEvent
+				? (event.activatorEvent as PointerEvent).clientX + (event.delta?.x ?? 0)
+				: undefined,
+		[],
+	);

Then call getScreenX(event) in both move handlers.

🤖 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/TimelineWrapper.tsx` around lines 435 -
468, Both onDragMove and onResizeMove duplicate the same screenX calculation;
extract it to a small helper (e.g. getScreenX) and call it from both handlers.
Create getScreenX(event: DragMoveEvent | ResizeMoveEvent) that checks
event.activatorEvent && "clientX" in event.activatorEvent and returns
(event.activatorEvent as PointerEvent).clientX + (event.delta?.x ?? 0) or
undefined, then replace the inline ternary in onDragMove and onResizeMove with
const screenX = getScreenX(event) and keep existing calls to showTooltip(span,
screenX).

199-297: 💤 Low value

snap math looks solid; tiny note on tie-breaks + per-move allocations

Threshold scaling with visible range is the right call, and the resize branches correctly bail when snapping would shrink below minItemDurationMs — good guardrail.

Two small things worth a glance:

  1. findNearest uses <=, so when two targets are equidistant, the later-inserted one wins. Given the insertion order (0/totalMs → hard regions → soft regions → playhead → keyframes), ties resolve to keyframes > playhead > soft > hard > bounds. Probably fine, but if you expected playhead/timeline-bounds to have priority on ties, flipping to < would do it.
  2. targetSet is rebuilt on every pointer move. For most projects this is nothing, but on a heavy timeline (lots of regions + many keyframes) it's allocating a Set and an Array per mousemove. The static portion (bounds + non-active region edges + keyframes) could be precomputed in a useMemo keyed on the same deps and just filtered for the active id at call time. Defer if not visible in profiling.
🤖 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/TimelineWrapper.tsx` around lines 199 -
297, The snapSpanToTargets logic needs two small fixes: (1) change the tie-break
in findNearest from "<=" to "<" so earlier-inserted targets win ties (flip from
<= to < in the comparison inside findNearest), and (2) avoid rebuilding
targetSet on every pointer move by memoizing the static targets (bounds,
allRegionSpans/softSnapSpans edges, keyframeTimesMs, currentTimeMs) with useMemo
and then filter out the activeItemId inside snapSpanToTargets before calling
findNearest; reference functions/ids: snapSpanToTargets, findNearest, targetSet,
allRegionSpans, softSnapSpans, keyframeTimesMs, useMemo.
🤖 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/TimelineWrapper.tsx`:
- Around line 435-468: Both onDragMove and onResizeMove duplicate the same
screenX calculation; extract it to a small helper (e.g. getScreenX) and call it
from both handlers. Create getScreenX(event: DragMoveEvent | ResizeMoveEvent)
that checks event.activatorEvent && "clientX" in event.activatorEvent and
returns (event.activatorEvent as PointerEvent).clientX + (event.delta?.x ?? 0)
or undefined, then replace the inline ternary in onDragMove and onResizeMove
with const screenX = getScreenX(event) and keep existing calls to
showTooltip(span, screenX).
- Around line 199-297: The snapSpanToTargets logic needs two small fixes: (1)
change the tie-break in findNearest from "<=" to "<" so earlier-inserted targets
win ties (flip from <= to < in the comparison inside findNearest), and (2) avoid
rebuilding targetSet on every pointer move by memoizing the static targets
(bounds, allRegionSpans/softSnapSpans edges, keyframeTimesMs, currentTimeMs)
with useMemo and then filter out the activeItemId inside snapSpanToTargets
before calling findNearest; reference functions/ids: snapSpanToTargets,
findNearest, targetSet, allRegionSpans, softSnapSpans, keyframeTimesMs, useMemo.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76ee3694-d13d-4dc7-9014-c24878491696

📥 Commits

Reviewing files that changed from the base of the PR and between b0293e7 and 9294b1f.

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

@AbhinRustagi
Copy link
Copy Markdown
Contributor Author

@siddharthvaddem This is ready for review

@quakeboy
Copy link
Copy Markdown

@AbhinRustagi - I saw the video, thanks for referencing the issue I created. Does it also snap to the timeline cursor? or only to edges of other clips? I use the snap to cursor a lot in Capcut and it's great for my editing workflow. Esp. after you find a frame, I place my cursor there, and then drag clips in other layer to the same frame, or sound effects etc.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add Snap Guides when resizing any of the rows in the timeline

3 participants