Skip to content

feat: spring-driven morph animation for askbar-to-chat transition#11

Merged
quiet-node merged 8 commits intomainfrom
feature/animation-morph
Apr 1, 2026
Merged

feat: spring-driven morph animation for askbar-to-chat transition#11
quiet-node merged 8 commits intomainfrom
feature/animation-morph

Conversation

@quiet-node
Copy link
Copy Markdown
Owner

Summary

  • Replaces Framer Motion's height: 'auto' snap with a spring-driven height animation (useMotionValue + useSpring) that smoothly tracks growing content during streaming
  • Adds Apple Sheet-style spring transition for the askbar→chat morph with matched cubic-bezier CSS transitions for box-shadow and border-radius
  • Rewrites auto-scroll to check scroll position fresh from the DOM on each content change, eliminating stale ref issues caused by spring-triggered layout events
  • Cleans up dead scroll-pinning ref (isUserNearBottomRef) and handleScroll callback that became unused after the auto-scroll rewrite

Test plan

  • All 137 tests pass
  • 100% code coverage maintained
  • ESLint clean (0 errors)
  • Manual QA: morph animation is smooth, auto-scroll works during streaming, scrolling up disengages auto-scroll

🤖 Generated with Claude Code

quiet-node and others added 8 commits March 31, 2026 23:11
Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@quiet-node quiet-node merged commit 512b7f2 into main Apr 1, 2026
3 checks passed
@quiet-node quiet-node deleted the feature/animation-morph branch April 1, 2026 19:19
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

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

* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

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

* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

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

* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

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

---------
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).


* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.


* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.


* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.


* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.


* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.


* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).


---------
quiet-node added a commit that referenced this pull request Apr 11, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

---------
This was referenced Apr 11, 2026
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.

1 participant