feat(MM): Make MotionSpec declarative and state-driven (3/3)

This change refactors `MotionValue` to make its `MotionSpec` (animation
physics) derived from a reactive lambda, replacing the previous
imperative approach.

Previously, `MotionValue` had a mutable `spec` property that consumers
would update directly. This required imperative logic within consumers
(`motionValue.spec = ...`) which did not align well with Compose's
declarative, state-driven architecture.

The `MotionValue` constructor now accepts a `spec: () -> MotionSpec`
lambda. The value now automatically reacts to changes in any state read
within this lambda, making its behavior inherently declarative.

Test: Tested on the previous MotionValueTests
Bug: 428886057
Flag: com.android.systemui.scene_container
Change-Id: I83c22591994cab1a65014d6d17543c37ce319b61
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionChangeDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionChangeDemo.kt
index dbb115f..fe12834 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionChangeDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionChangeDemo.kt
@@ -51,15 +51,14 @@
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.effects.FixedValue
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.Mapping
-import com.android.mechanics.spec.MotionSpec
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
 import com.android.mechanics.spec.builder.spatialMotionSpec
 
 object DirectionChangeDemo : Demo<Unit>, HasMotionValueVisualization {
 
-    var inputRange by mutableStateOf(0f..0f)
+    private var inputRange by mutableStateOf(0f..0f)
 
     @Composable
     override fun DemoUi(config: Unit, modifier: Modifier) {
@@ -67,8 +66,20 @@
 
         // Also using GestureContext.dragOffset as input.
         val gestureContext = rememberDistanceGestureContext()
-        val spec = rememberSpec(inputOutputRange = inputRange)
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        spatialMotionSpec(baseMapping = Mapping.Fixed(inputRange.start)) {
+                            after(
+                                (inputRange.start + inputRange.endInclusive) / 2f,
+                                FixedValue(inputRange.endInclusive),
+                            )
+                        }
+                    },
+            )
 
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
@@ -134,22 +145,6 @@
         }
     }
 
-    @Composable
-    fun rememberSpec(inputOutputRange: ClosedFloatingPointRange<Float>): MotionSpec {
-
-        val builderContext = rememberMotionBuilderContext()
-        return remember(inputOutputRange, builderContext) {
-            with(builderContext) {
-                spatialMotionSpec(baseMapping = Mapping.Fixed(inputOutputRange.start)) {
-                    after(
-                        (inputOutputRange.start + inputOutputRange.endInclusive) / 2f,
-                        FixedValue(inputOutputRange.endInclusive),
-                    )
-                }
-            }
-        }
-    }
-
     @Composable override fun rememberDefaultConfig() {}
 
     override val visualizationInputRange: ClosedFloatingPointRange<Float>
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionSpecDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionSpecDemo.kt
index 3896bcf..4a608f2 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionSpecDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/DirectionSpecDemo.kt
@@ -53,6 +53,7 @@
 import com.android.mechanics.demo.tuneable.Demo
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.Breakpoint
 import com.android.mechanics.spec.BreakpointKey
@@ -62,7 +63,7 @@
 import com.android.mechanics.spec.OnChangeSegmentHandler
 import com.android.mechanics.spec.SegmentData
 import com.android.mechanics.spec.SegmentKey
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
+import com.android.mechanics.spec.builder.MotionBuilderContext
 import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec
 
 object DirectionSpecDemo : Demo<Unit>, HasMotionValueVisualization {
@@ -80,8 +81,12 @@
 
         // Also using GestureContext.dragOffset as input.
         val gestureContext = rememberDistanceGestureContext()
-        val spec = rememberSpec(inputOutputRange = inputRange)
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec = rememberMotionSpecAsState { buildSpec(inputOutputRange = inputRange) },
+            )
 
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
@@ -147,55 +152,46 @@
         }
     }
 
-    @Composable
-    fun rememberSpec(inputOutputRange: ClosedFloatingPointRange<Float>): MotionSpec {
+    private fun MotionBuilderContext.buildSpec(
+        inputOutputRange: ClosedFloatingPointRange<Float>
+    ): MotionSpec {
         val delta = inputOutputRange.endInclusive - inputOutputRange.start
 
         val startPosPx = inputOutputRange.start
         val detachPosPx = delta * .4f
         val attachPosPx = delta * .1f
 
-        val builderContext = rememberMotionBuilderContext()
-
-        return remember(inputOutputRange, builderContext) {
-            with(builderContext) {
-                val detachSpec =
-                    spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
-                        fractionalInputFromCurrent(startPosPx, fraction = .3f, key = Keys.Start)
-                        identity(detachPosPx, key = Keys.Detach, spring = spatial.slow)
-                    }
-
-                val attachSpec =
-                    spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
-                        identity(attachPosPx, key = Keys.Detach, spring = spatial.fast)
-                    }
-
-                val segmentHandlers =
-                    mapOf<SegmentKey, OnChangeSegmentHandler>(
-                        SegmentKey(Keys.Detach, Keys.End, InputDirection.Min) to
-                            { currentSegment, _, newDirection ->
-                                if (newDirection != currentSegment.direction) currentSegment
-                                else null
-                            },
-                        SegmentKey(Keys.Start, Keys.Detach, InputDirection.Max) to
-                            {
-                                currentSegment: SegmentData,
-                                newInput: Float,
-                                newDirection: InputDirection ->
-                                if (newDirection != currentSegment.direction && newInput >= 0)
-                                    currentSegment
-                                else null
-                            },
-                    )
-
-                MotionSpec(
-                    maxDirection = detachSpec,
-                    minDirection = attachSpec,
-                    resetSpring = spatial.default,
-                    segmentHandlers = segmentHandlers,
-                )
+        val detachSpec =
+            spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
+                fractionalInputFromCurrent(startPosPx, fraction = .3f, key = Keys.Start)
+                identity(detachPosPx, key = Keys.Detach, spring = spatial.slow)
             }
-        }
+
+        val attachSpec =
+            spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
+                identity(attachPosPx, key = Keys.Detach, spring = spatial.fast)
+            }
+
+        val segmentHandlers =
+            mapOf<SegmentKey, OnChangeSegmentHandler>(
+                SegmentKey(Keys.Detach, Keys.End, InputDirection.Min) to
+                    { currentSegment, _, newDirection ->
+                        if (newDirection != currentSegment.direction) currentSegment else null
+                    },
+                SegmentKey(Keys.Start, Keys.Detach, InputDirection.Max) to
+                    { currentSegment: SegmentData, newInput: Float, newDirection: InputDirection ->
+                        if (newDirection != currentSegment.direction && newInput >= 0)
+                            currentSegment
+                        else null
+                    },
+            )
+
+        return MotionSpec(
+            maxDirection = detachSpec,
+            minDirection = attachSpec,
+            resetSpring = spatial.default,
+            segmentHandlers = segmentHandlers,
+        )
     }
 
     @Composable override fun rememberDefaultConfig() {}
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeBoxDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeBoxDemo.kt
index 934d259..e1aafe9 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeBoxDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeBoxDemo.kt
@@ -58,10 +58,12 @@
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.demo.tuneable.SpringParameterSection
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.Guarantee
 import com.android.mechanics.spec.Mapping
 import com.android.mechanics.spec.MotionSpec
+import com.android.mechanics.spec.builder.MotionBuilderContext
 import com.android.mechanics.spec.builder.rememberMotionBuilderContext
 import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec
 import com.android.mechanics.spring.SpringParameters
@@ -93,15 +95,21 @@
 
         // Also using GestureContext.dragOffset as input.
         val gestureContext = rememberDistanceGestureContext()
-        val spec =
-            rememberSpec(
-                activeScenario,
-                { placedBoxX },
-                { placedBoxWidth },
-                inputOutputRange = inputRange,
-                config,
+
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        buildSpec(
+                            scenario = activeScenario,
+                            x = { placedBoxX },
+                            width = { placedBoxWidth },
+                            config = config,
+                        )
+                    },
             )
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
             modifier = modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 48.dp),
@@ -202,63 +210,52 @@
         }
     }
 
-    @Composable
-    fun rememberSpec(
+    private fun MotionBuilderContext.buildSpec(
         scenario: Scenario,
         x: () -> Float,
         width: () -> Float,
-        inputOutputRange: ClosedFloatingPointRange<Float>,
         config: Config,
     ): MotionSpec {
-
-        val builderContext = rememberMotionBuilderContext()
         val left = x()
         val widthVal = width()
         val right = left + widthVal
 
-        return remember(scenario, inputOutputRange, config, left, widthVal, builderContext) {
-            with(builderContext) {
-                val guarantee = Guarantee.InputDelta(config.guaranteeDistance.toPx())
-                val minSize = config.minVisibleWidth.toPx()
-                when (scenario) {
-                    Scenario.Mapped ->
-                        MotionSpec(
-                            spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
-                                target(breakpoint = left, from = 0f, to = widthVal)
-                                fixedValue(breakpoint = right, value = widthVal)
-                            }
-                        )
+        val guarantee = Guarantee.InputDelta(config.guaranteeDistance.toPx())
+        val minSize = config.minVisibleWidth.toPx()
 
-                    Scenario.Triggered ->
-                        MotionSpec(
-                            spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
-                                target(
-                                    breakpoint = min(left + minSize, right),
-                                    from = minSize,
-                                    to = widthVal - minSize,
-                                )
-                                fixedValue(breakpoint = right, value = widthVal)
-                            }
-                        )
+        return when (scenario) {
+            Scenario.Mapped ->
+                MotionSpec(
+                    spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
+                        target(breakpoint = left, from = 0f, to = widthVal)
+                        fixedValue(breakpoint = right, value = widthVal)
+                    }
+                )
 
-                    Scenario.Guaranteed ->
-                        MotionSpec(
-                            spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
-                                target(
-                                    breakpoint = min(left + minSize, right),
-                                    from = minSize,
-                                    to = widthVal - minSize,
-                                    guarantee = guarantee,
-                                )
-                                fixedValue(
-                                    breakpoint = right,
-                                    value = widthVal,
-                                    guarantee = guarantee,
-                                )
-                            }
+            Scenario.Triggered ->
+                MotionSpec(
+                    spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
+                        target(
+                            breakpoint = min(left + minSize, right),
+                            from = minSize,
+                            to = widthVal - minSize,
                         )
-                }
-            }
+                        fixedValue(breakpoint = right, value = widthVal)
+                    }
+                )
+
+            Scenario.Guaranteed ->
+                MotionSpec(
+                    spatialDirectionalMotionSpec(initialMapping = Mapping.Zero) {
+                        target(
+                            breakpoint = min(left + minSize, right),
+                            from = minSize,
+                            to = widthVal - minSize,
+                            guarantee = guarantee,
+                        )
+                        fixedValue(breakpoint = right, value = widthVal, guarantee = guarantee)
+                    }
+                )
         }
     }
 
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeFadeDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeFadeDemo.kt
index 1d89fe7..d448fcb 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeFadeDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/GuaranteeFadeDemo.kt
@@ -47,11 +47,11 @@
 import com.android.mechanics.demo.tuneable.Demo
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.Guarantee
 import com.android.mechanics.spec.MotionSpec
 import com.android.mechanics.spec.builder.effectsDirectionalMotionSpec
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
 
 object GuaranteeFadeDemo : Demo<Unit>, HasMotionValueVisualization {
 
@@ -67,15 +67,48 @@
 
         // Also using GestureContext.dragOffset as input.
         val gestureContext = rememberDistanceGestureContext()
-        val spec = rememberSpec(inputOutputRange = inputRange, { 0f })
-        val guaranteeSpec =
-            rememberSpec(inputOutputRange = inputRange, guaranteeDistance::floatValue)
 
         val withoutGuarantee =
-            rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        MotionSpec(
+                            effectsDirectionalMotionSpec {
+                                fixedValue(
+                                    breakpoint = (inputRange.start + inputRange.endInclusive) / 2f,
+                                    value = 1f,
+                                    guarantee = Guarantee.None,
+                                )
+                            }
+                        )
+                    },
+            )
 
         val withGuarantee =
-            rememberMotionValue(gestureContext::dragOffset, { guaranteeSpec }, gestureContext)
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        val distance = guaranteeDistance.floatValue
+                        MotionSpec(
+                            effectsDirectionalMotionSpec {
+                                fixedValue(
+                                    breakpoint = (inputRange.start + inputRange.endInclusive) / 2f,
+                                    value = 1f,
+                                    guarantee =
+                                        if (distance > 0) {
+                                            Guarantee.InputDelta(distance)
+                                        } else {
+                                            Guarantee.None
+                                        },
+                                )
+                            }
+                        )
+                    },
+            )
 
         val defaultValueColor = MaterialTheme.colorScheme.primary
         val guaranteeValueColor = MaterialTheme.colorScheme.secondary
@@ -136,31 +169,6 @@
         }
     }
 
-    @Composable
-    fun rememberSpec(
-        inputOutputRange: ClosedFloatingPointRange<Float>,
-        guaranteeDistance: () -> Float,
-    ): MotionSpec {
-        val distance = guaranteeDistance()
-        val guarantee = if (distance > 0) Guarantee.InputDelta(distance) else Guarantee.None
-        val builderContext = rememberMotionBuilderContext()
-
-        return remember(guarantee, inputOutputRange, builderContext) {
-            with(builderContext) {
-                MotionSpec(
-                    effectsDirectionalMotionSpec {
-                        fixedValue(
-                            breakpoint =
-                                (inputOutputRange.start + inputOutputRange.endInclusive) / 2f,
-                            value = 1f,
-                            guarantee = guarantee,
-                        )
-                    }
-                )
-            }
-        }
-    }
-
     @Composable override fun rememberDefaultConfig() {}
 
     override val visualizationInputRange: ClosedFloatingPointRange<Float>
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachDemo.kt
index d902619..bfd70bb 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachDemo.kt
@@ -52,8 +52,8 @@
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.effects.MagneticDetach
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
 import com.android.mechanics.spec.builder.spatialMotionSpec
 
 object MagneticDetachDemo : Demo<Unit>, HasMotionValueVisualization {
@@ -64,12 +64,15 @@
         val colors = MaterialTheme.colorScheme
 
         val gestureContext = rememberDistanceGestureContext()
-        val motionBuilderContext = rememberMotionBuilderContext()
-        val spec =
-            remember(motionBuilderContext) {
-                motionBuilderContext.spatialMotionSpec { after(50.dp.toPx(), MagneticDetach()) }
-            }
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        spatialMotionSpec { after(50.dp.toPx(), MagneticDetach()) }
+                    },
+            )
 
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachWithOverdragDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachWithOverdragDemo.kt
index 0c05ff2..1db79ab 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachWithOverdragDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/MagneticDetachWithOverdragDemo.kt
@@ -52,12 +52,12 @@
 import com.android.mechanics.effects.MagneticDetach
 import com.android.mechanics.effects.Overdrag
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.InputDirection
 import com.android.mechanics.spec.SemanticKey
 import com.android.mechanics.spec.builder.MotionBuilderContext
 import com.android.mechanics.spec.builder.fixedSpatialValueSpec
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
 import com.android.mechanics.spec.builder.spatialMotionSpec
 
 object MagneticDetachWithOverdragDemo : Demo<Unit>, HasMotionValueVisualization {
@@ -68,10 +68,20 @@
     override fun DemoUi(config: Unit, modifier: Modifier) {
         val colors = MaterialTheme.colorScheme
         val gestureContext = rememberDistanceGestureContext()
-        val motionBuilderContext = rememberMotionBuilderContext()
-        var spec by remember() { mutableStateOf(motionBuilderContext.fixedSpatialValueSpec(0f)) }
+        var dragState: DragState by remember { mutableStateOf(DragState.Idle(targetValue = 0f)) }
 
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        when (val dragState = dragState) {
+                            is DragState.Idle -> fixedSpatialValueSpec(dragState.targetValue)
+                            DragState.Dragging -> createDragSpec()
+                        }
+                    },
+            )
 
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
@@ -110,11 +120,11 @@
                                 Orientation.Horizontal,
                                 onDragStarted = {
                                     gestureContext.reset(motionValue.output, InputDirection.Max)
-                                    spec = motionBuilderContext.createDragSpec()
+                                    dragState = DragState.Dragging
                                 },
                                 onDragStopped = {
                                     val targetValue = motionValue[TargetValue] ?: motionValue.output
-                                    spec = motionBuilderContext.fixedSpatialValueSpec(targetValue)
+                                    dragState = DragState.Idle(targetValue = targetValue)
                                 },
                             )
                             .debugMotionValue(motionValue)
@@ -135,6 +145,12 @@
     override val identifier: String = "MagneticDetachOverdrag"
 
     val TargetValue = SemanticKey<Float?>()
+
+    private sealed interface DragState {
+        data class Idle(val targetValue: Float) : DragState
+
+        data object Dragging : DragState
+    }
 }
 
 private fun MotionBuilderContext.createDragSpec() = spatialMotionSpec {
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/SpecDemo.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/SpecDemo.kt
index 4a5c981..43b0444 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/SpecDemo.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/presentation/SpecDemo.kt
@@ -55,12 +55,13 @@
 import com.android.mechanics.demo.tuneable.HasMotionValueVisualization
 import com.android.mechanics.demo.tuneable.LabelledCheckbox
 import com.android.mechanics.rememberDistanceGestureContext
+import com.android.mechanics.rememberMotionSpecAsState
 import com.android.mechanics.rememberMotionValue
 import com.android.mechanics.spec.DirectionalMotionSpec
 import com.android.mechanics.spec.Guarantee
 import com.android.mechanics.spec.Mapping
 import com.android.mechanics.spec.MotionSpec
-import com.android.mechanics.spec.builder.rememberMotionBuilderContext
+import com.android.mechanics.spec.builder.MotionBuilderContext
 import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec
 
 object SpecDemo : Demo<SpecDemo.Config>, HasMotionValueVisualization {
@@ -82,8 +83,15 @@
 
         // Also using GestureContext.dragOffset as input.
         val gestureContext = rememberDistanceGestureContext()
-        val spec = rememberSpec(activeScenario, config, inputOutputRange = inputRange)
-        val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
+        val motionValue =
+            rememberMotionValue(
+                input = { gestureContext.dragOffset },
+                gestureContext = gestureContext,
+                spec =
+                    rememberMotionSpecAsState {
+                        buildSpec(activeScenario, config, inputOutputRange = inputRange)
+                    },
+            )
 
         Column(
             verticalArrangement = Arrangement.spacedBy(24.dp),
@@ -153,75 +161,59 @@
         }
     }
 
-    @Composable
-    fun rememberSpec(
+    private fun MotionBuilderContext.buildSpec(
         scenario: Scenario,
         config: Config,
         inputOutputRange: ClosedFloatingPointRange<Float>,
     ): MotionSpec {
+        return MotionSpec(
+            when (scenario) {
+                Scenario.Empty -> DirectionalMotionSpec.Empty
+                Scenario.Toggle ->
+                    spatialDirectionalMotionSpec(Mapping.Fixed(inputOutputRange.start)) {
+                        fixedValue(
+                            breakpoint =
+                                (inputOutputRange.start + inputOutputRange.endInclusive) / 2f,
+                            value = inputOutputRange.endInclusive,
+                        )
+                    }
 
-        val builderContext = rememberMotionBuilderContext()
+                Scenario.Steps ->
+                    spatialDirectionalMotionSpec(Mapping.Fixed(inputOutputRange.start)) {
+                        val steps = 8
+                        val stepSize =
+                            (inputOutputRange.start + inputOutputRange.endInclusive) / steps
 
-        return remember(scenario, inputOutputRange, config, builderContext) {
-            MotionSpec(
-                when (scenario) {
-                    Scenario.Empty -> DirectionalMotionSpec.Empty
-                    Scenario.Toggle ->
-                        builderContext.spatialDirectionalMotionSpec(
-                            Mapping.Fixed(inputOutputRange.start)
-                        ) {
+                        val guarantee =
+                            if (config.stepGuarantee) Guarantee.InputDelta(stepSize)
+                            else Guarantee.None
+
+                        val outDiff =
+                            (inputOutputRange.start + inputOutputRange.endInclusive) / (steps - 1)
+                        repeat(steps - 2) { step ->
                             fixedValue(
-                                breakpoint =
-                                    (inputOutputRange.start + inputOutputRange.endInclusive) / 2f,
-                                value = inputOutputRange.endInclusive,
-                            )
-                        }
-
-                    Scenario.Steps ->
-                        builderContext.spatialDirectionalMotionSpec(
-                            Mapping.Fixed(inputOutputRange.start)
-                        ) {
-                            val steps = 8
-                            val stepSize =
-                                (inputOutputRange.start + inputOutputRange.endInclusive) / steps
-
-                            val guarantee =
-                                if (config.stepGuarantee) Guarantee.InputDelta(stepSize)
-                                else Guarantee.None
-
-                            val outDiff =
-                                (inputOutputRange.start + inputOutputRange.endInclusive) /
-                                    (steps - 1)
-                            repeat(steps - 2) { step ->
-                                fixedValue(
-                                    breakpoint = (step + 1) * stepSize,
-                                    value = (step + 1) * outDiff,
-                                    guarantee = guarantee,
-                                )
-                            }
-
-                            fixedValue(
-                                breakpoint = inputOutputRange.endInclusive - stepSize,
-                                value = inputOutputRange.endInclusive,
+                                breakpoint = (step + 1) * stepSize,
+                                value = (step + 1) * outDiff,
                                 guarantee = guarantee,
                             )
                         }
 
-                    Scenario.TrackNSnap ->
-                        builderContext.spatialDirectionalMotionSpec(
-                            Mapping.Fixed(inputOutputRange.start)
-                        ) {
-                            val third = (inputOutputRange.start + inputOutputRange.endInclusive) / 3
+                        fixedValue(
+                            breakpoint = inputOutputRange.endInclusive - stepSize,
+                            value = inputOutputRange.endInclusive,
+                            guarantee = guarantee,
+                        )
+                    }
 
-                            target(third, from = third, to = 2 * third)
-                            fixedValue(
-                                breakpoint = 2 * third,
-                                value = inputOutputRange.endInclusive,
-                            )
-                        }
-                }
-            )
-        }
+                Scenario.TrackNSnap ->
+                    spatialDirectionalMotionSpec(Mapping.Fixed(inputOutputRange.start)) {
+                        val third = (inputOutputRange.start + inputOutputRange.endInclusive) / 3
+
+                        target(third, from = third, to = 2 * third)
+                        fixedValue(breakpoint = 2 * third, value = inputOutputRange.endInclusive)
+                    }
+            }
+        )
     }
 
     @Composable