Skip to content

Comments

Fix drag constraints getting stuck when clicked during animation#3456

Merged
mattgperry merged 3 commits intomainfrom
drag-constraints-stuck
Jan 7, 2026
Merged

Fix drag constraints getting stuck when clicked during animation#3456
mattgperry merged 3 commits intomainfrom
drag-constraints-stuck

Conversation

@mattgperry
Copy link
Collaborator

@mattgperry mattgperry commented Jan 6, 2026

Summary

  • Fixes issue where draggable elements with ref-based constraints get stuck outside boundaries when clicked during constraint animation
  • Always pause (not stop) animations on pointerdown so they can be resumed if no drag starts
  • Resume paused animation when user releases without dragging (just clicking)

Fixes #2697
Closes #3394

Test plan

  • Added new Cypress test drag-constraints-return.tsx that verifies:
    • Element returns to constraints when released outside bounds (baseline)
    • Element returns to constraints when clicked during return animation (the bug fix)
  • All 23 drag tests pass
  • Manually verified the fix resolves the reported issue

🤖 Generated with Claude Code

When a draggable element with ref-based constraints is dragged outside
its boundaries (with dragElastic), it should animate back within the
constraints when released. However, if the user clicks on the element
while it's animating back, the animation was being stopped and the
element would get "stuck" outside the constraints.

The fix:
1. Always pause (not stop) animations when pointerdown occurs, so the
   animation can be resumed if no drag starts
2. Resume the paused animation when the user releases without dragging
   (just clicking), ensuring the constraint animation continues

Fixes #2697

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link

claude bot commented Jan 6, 2026

Code Review

I've reviewed PR #3456 which fixes the drag constraints bug where elements get stuck when clicked during constraint return animations. Overall, this is a well-implemented fix with excellent test coverage. Here's my detailed feedback:

✅ Strengths

1. Root Cause Analysis & Fix
The fix correctly identifies that the issue stems from stopping animations on pointerdown rather than pausing them. The changes in VisualElementDragControls.ts:111-114 and PanSession.ts:183-187 elegantly solve the problem:

  • Always pause (not stop) animations on pointer down, preserving animation state
  • Resume paused animations when the user releases without dragging (just clicking)
  • This allows constraint animations to continue when interrupted by a click

2. Excellent Test Coverage
The new Cypress tests in drag-constraints-return.tsx are comprehensive:

  • Test baseline behavior (element returns to constraints)
  • Test the specific bug scenario (click during animation)
  • Well-documented with clear comments
  • Appropriate wait times and assertion tolerances (±2px margin for floating-point precision)

3. Code Quality

  • Clean, minimal changes that follow existing patterns
  • Well-commented explaining the "why" not just the "what"
  • Consistent with repository style (using interface, named exports, arrow callbacks)

🔍 Observations & Questions

1. Simplified Logic in VisualElementDragControls (Lines 111-114)

// Before: Conditional logic based on dragSnapToOrigin
dragSnapToOrigin ? this.pauseAnimation() : this.stopAnimation()

// After: Always pause
this.pauseAnimation()

This simplification is good, but I'm curious about the implications:

  • Previously, non-dragSnapToOrigin elements had animations fully stopped on pointerdown
  • Now all drag elements have animations paused instead
  • Question: Could this behavior change affect other drag scenarios? Might be worth checking if any existing tests implicitly relied on the old stopAnimation() behavior.

2. Resume Condition in PanSession (Lines 183-187)

if (this.dragSnapToOrigin || !this.startEvent) {
    resumeAnimation && resumeAnimation()
}

The !this.startEvent check is clever - it detects when the user clicked without moving (no drag started). This is the key to fixing the bug.

  • Minor: Consider adding a comment explaining what !this.startEvent indicates (e.g., "no drag started, user just clicked")

3. Test Precision
The test assertions use a ±2px tolerance which is reasonable for animation precision:

expect(right).to.be.at.most(302)  // 300 + 2px tolerance
expect(left).to.be.at.least(-2)   // 0 - 2px tolerance

This is good defensive testing accounting for sub-pixel rendering and timing variations.

🎯 Suggestions

1. Edge Case Testing
Consider adding tests for these scenarios (if not covered elsewhere):

  • Multiple rapid clicks during animation
  • Click-and-hold (pointerdown without immediate release) during animation
  • Touch events on mobile (if applicable)

2. Performance Consideration
The change from stopAnimation() to pauseAnimation() means animation state is preserved in memory. This is negligible for most cases, but worth noting:

  • Paused animations maintain their internal state
  • For apps with hundreds of draggable elements, this could have minor memory implications
  • Probably not an issue in practice, just worth documenting if this becomes a pattern

3. Documentation
Consider updating any drag-related documentation or API references to clarify:

  • The new pause/resume behavior on click
  • That clicking a draggable element during constraint animation now resumes the animation

🔒 Security & Performance

  • No security concerns identified - changes are isolated to gesture handling logic
  • No performance regressions expected - pausing vs stopping is similar performance-wise
  • No breaking changes - the fix corrects broken behavior without changing the public API

📝 Minor Nits

  1. Test file location: The test file is in dev/react/src/tests/ while the Cypress test is in packages/framer-motion/cypress/integration/. This is fine if it follows the repo's test organization pattern.

  2. Comment consistency: The comment in VisualElementDragControls.ts:112-113 could be more concise:

    // Pause animations to allow throw-and-catch and resume if click-only (no drag)

✅ Final Verdict

LGTM with minor suggestions! This is a solid fix that:

  • ✅ Correctly identifies and fixes the root cause
  • ✅ Includes comprehensive test coverage
  • ✅ Follows repository conventions
  • ✅ Has no apparent security or performance issues
  • ✅ Maintains backward compatibility

The fix is minimal, well-tested, and addresses the reported issue (#2697) effectively. The 23 passing drag tests mentioned in the PR description give confidence this doesn't break existing functionality.

Recommendation: Approve and merge after considering the questions about potential behavior changes in other drag scenarios (likely already verified by existing tests passing).

Great work! 🎉

When snapToCursor is used, we need to STOP the animation (not pause)
because snapToCursor sets new position values. A paused animation's
state would interfere with the new values being set.

The pause behavior is still needed for the constraint animation fix
when snapToCursor is not used.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link

claude bot commented Jan 7, 2026

PR Review: Fix drag constraints getting stuck when clicked during animation

Summary

This PR fixes a bug where draggable elements with ref-based constraints would get stuck outside boundaries when clicked during the constraint animation. The fix is well-implemented with good test coverage.


Code Quality & Best Practices ✅

Strengths:

  • Clear, focused changes that address a specific bug
  • Code follows the repository's style guide (named exports, interfaces, arrow callbacks)
  • Excellent explanatory comments in the changed logic (e.g., lines 112-114 in VisualElementDragControls.ts and lines 183-184 in PanSession.ts)
  • Changes are minimal and targeted - only touching what's necessary

Minor observations:

  • The logic is well-reasoned: pausing instead of stopping animations allows them to be resumed if no drag starts
  • The condition !this.startEvent in PanSession.ts:185 is a clean way to detect "just a click"

Logic & Implementation ✅

VisualElementDragControls.ts changes (lines 111-121):
The refactored onSessionStart logic is clearer:

  • When snapToCursor is enabled: stop animations (set new position values)
  • Otherwise: pause animations (allows resume if no drag happens)

This is a good improvement over the previous dragSnapToOrigin-based approach.

PanSession.ts changes (lines 183-187):
The added condition || !this.startEvent correctly identifies when a user clicked without dragging. The logic is:

  • If dragSnapToOrigin is set: resume animation (existing behavior)
  • If no drag started (!this.startEvent): resume animation (new fix)

This ensures constraint animations continue when interrupted by a click.


Test Coverage ✅

Excellent test coverage:

  1. Baseline test (returns to constraints when released outside): Verifies existing functionality still works
  2. Bug fix test (returns to constraints when clicked during animation): Directly tests the reported issue

Test implementation notes:

  • Good use of waits to allow animations to complete
  • Proper tolerance in boundary checks (±2 pixels) to account for floating-point precision
  • Tests both the happy path and the bug scenario

Suggestions:

  • Consider adding a test case with dragSnapToOrigin={true} to ensure that path still works correctly
  • Could add a test verifying that a real drag (not just a click) during constraint animation also works as expected

Performance Considerations ✅

No performance concerns:

  • The changes don't introduce new loops or expensive operations
  • Using pause() instead of stop() is actually more efficient as it preserves animation state
  • The added condition check (!this.startEvent) is a simple boolean check with negligible cost

Security Concerns ✅

No security issues identified. The changes:

  • Don't introduce user input handling beyond what already exists
  • Don't modify event validation logic
  • Work within the existing gesture system's safety mechanisms

Potential Issues & Edge Cases

Minor consideration:
In VisualElementDragControls.ts:119, pauseAnimation() is called, which iterates through axes and pauses animations. If there's no animation running, this is a no-op (due to the optional chaining in line 510). This is fine, but worth noting.

Edge case to verify manually:

  • What happens if multiple rapid clicks occur during the constraint animation? The current implementation should handle this gracefully (each click pauses, then resumes), but it would be good to verify.

Suggestions

  1. Test enhancement: Add a test case for dragSnapToOrigin={true} to ensure backward compatibility:

    it("Returns to origin when dragSnapToOrigin is enabled and clicked during animation", () => {
      // Test implementation
    })
  2. Documentation: Consider adding a comment in the test file explaining what the bug was (elements getting stuck outside constraints when clicked during return animation).

  3. Type safety: The getAnimationState method (line 513-515) could potentially return undefined. Consider if this needs explicit handling, though the current usage seems safe.


Overall Assessment

Verdict: Approved ✅

This is a well-crafted bug fix that:

The fix is elegant: by pausing instead of stopping animations, and resuming them when a click (not drag) occurs, constraint animations can properly continue after being interrupted.

Great work!

When dragging an element during a constraint animation (interrupting the
animation with a new drag), the element would jump to the wrong position
on release. The jump size was proportional to the second drag gesture size.

Root cause: When a drag started during a paused animation, the motion value's
get() returned stale data from the paused animation state instead of the
current rendered position. This caused originPoint to be set incorrectly.

Fix: Call stopAnimation() in onStart (when actual drag begins) to properly
sync the motion value to its current position before recording originPoint.

The animation lifecycle is now:
1. onSessionStart: pauseAnimation() - allows resume if no drag starts (click)
2. onStart (if drag begins): stopAnimation() - sync value for accurate originPoint
3. During drag: values set directly via set()
4. onEnd: startAnimation() - new constraint animation from correct position

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link

claude bot commented Jan 7, 2026

Code Review

I've reviewed PR #3456 and overall this is a well-crafted fix for a tricky edge case. The solution is elegant and the test coverage is excellent. Below are my findings:

✅ Strengths

1. Clear Problem Understanding
The fix correctly addresses the root cause: when a user clicks during a constraint animation, the old approach would stop() animations (clearing animation state), causing the element to get stuck. The new approach uses pause() to preserve animation state for potential resumption.

2. Excellent Test Coverage
The three Cypress tests comprehensively cover:

  • Baseline behavior (returns to constraints when released)
  • The specific bug (returns when clicked during animation)
  • Edge case validation (no jump when re-dragged during animation)

The third test is particularly good at catching regressions with its 200px drag movement verification.

3. Minimal, Surgical Changes
The fix touches only the necessary code paths with clear comments explaining the behavior. Good adherence to the repository's coding style (interfaces, arrow functions, strict equality).


🔍 Code Quality Observations

VisualElementDragControls.ts (lines 111-126)
The logic flow is clear and well-commented. The conditional separation between snapToCursor (stop) vs normal (pause) makes sense:

  • snapToCursor: Must stop because new position values will be set
  • Normal: Pause to allow resume if no drag starts

PanSession.ts (lines 183-187)
The condition this.dragSnapToOrigin || !this.startEvent correctly handles both:

  • dragSnapToOrigin: Original functionality preserved
  • !this.startEvent: New fix - no drag started, so resume animation

💡 Potential Considerations

1. Animation State Management
The flow involves multiple state transitions:

pointerdown → pause animation
drag starts → stop animation  
drag ends without starting → resume animation

Question: Is there any scenario where pauseAnimation() could be called but the paused animation is never resumed or stopped? For instance:

  • Component unmounts while animation is paused
  • presenceContext.isPresent becomes false mid-gesture

Recommendation: Consider adding cleanup in cancel() method to ensure paused animations don't leak:

cancel() {
    this.isDragging = false
    // ... existing code ...
    this.stopAnimation() // Ensure any paused animations are cleaned up
    // ... rest of code ...
}

2. Memory/Performance
Pausing animations instead of stopping them means keeping animation state in memory longer. This is likely negligible, but worth noting for elements with many drag interactions.

3. Test Timing
The tests use hardcoded waits (50ms, 100ms, 2000ms). While this is common in E2E tests:

  • Consider if 2000ms wait is sufficient across all CI environments
  • The -2 tolerance in assertions (lines 27-28, 61-62) suggests potential for flakiness

🔒 Security & Performance

Security: ✅ No concerns. Changes are internal logic only, no user input handling modified.

Performance: ✅ Minimal impact:

  • pause() vs stop() has negligible performance difference
  • The additional !this.startEvent check in PanSession is O(1)

📝 Minor Suggestions

1. Code Documentation
The comments are good, but consider documenting the state machine more explicitly:

// Animation state flow:
// 1. pointerdown → pause (preserves state for resume)
// 2. drag starts → stop (motion values need to reflect current position)
// 3. pointerup without drag → resume (continue constraint animation)

2. Test File Naming
The test component drag-constraints-return.tsx is well-named and focused.

3. Type Safety
Line 123 in VisualElementDragControls: The comment says "pauseAnimation was called" but it could be more precise about when it wasn't called (snapToCursor case).


✅ Final Verdict

This PR successfully fixes issue #2697 with:

  • ✅ Correct root cause analysis
  • ✅ Minimal code changes
  • ✅ Comprehensive test coverage
  • ✅ Clear comments
  • ✅ No security or performance concerns
  • ✅ Follows repository conventions

Recommendation: Approve with minor suggestion to add cleanup in cancel() method.

The only substantive suggestion is ensuring paused animations are cleaned up in edge cases (component unmount, presence exit). Otherwise, this is production-ready.


Great work on this fix! The state machine approach (pause → potentially resume) is much cleaner than the previous behavior.

@mattgperry mattgperry merged commit 210f72b into main Jan 7, 2026
4 checks passed
@mattgperry mattgperry deleted the drag-constraints-stuck branch January 7, 2026 10:43
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.

[BUG] Drag-Element gets stuck when clicked while animating

1 participant