Merge "Fix NavArgument equality check for Array args" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt
index 2778116..2ee1cc7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt
@@ -153,6 +153,9 @@
                 "org.gradle.daemon",
                 "android.builder.sdkDownload",
                 "android.suppressUnsupportedCompileSdk",
+                // added for paging-playground which needs to disable config cache
+                // due to b/325886853
+                "org.gradle.configuration-cache"
             )
 
         /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 4f8e229..d09eee3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -356,6 +356,14 @@
         inputConfigData: List<InputConfigData>,
         cameraId: String
     ): InputConfiguration {
+        check(inputConfigData.isNotEmpty()) {
+            "Call to create InputConfiguration but list of InputConfigData is empty."
+        }
+
+        if (inputConfigData.size == 1) {
+            val inputData = inputConfigData.first();
+            return InputConfiguration(inputData.width, inputData.height, inputData.format)
+        }
         val multiResolutionInput = inputConfigData.map { input ->
             MultiResolutionStreamInfo(input.width, input.height, cameraId)
         }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/GarbageCollectionUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/GarbageCollectionUtil.java
index 4b93909..65f9a8e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/GarbageCollectionUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/GarbageCollectionUtil.java
@@ -16,8 +16,12 @@
 
 package androidx.camera.testing.impl;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+
 import java.lang.ref.PhantomReference;
 import java.lang.ref.ReferenceQueue;
 import java.util.concurrent.TimeoutException;
@@ -35,21 +39,44 @@
      * Causes garbage collection and ensures finalization has run before returning.
      */
     public static void runFinalization() throws TimeoutException, InterruptedException {
+        runFinalization(NUM_GC_ITERATIONS);
+    }
+
+    /**
+     * Runs garbage collection and ensures finalization for a specified number of iterations.
+     */
+    private static void runFinalization(int numGcIterations)
+            throws TimeoutException, InterruptedException {
         ReferenceQueue<Object> finalizeAwaitQueue = new ReferenceQueue<>();
         PhantomReference<Object> finalizeSignal;
         // Ensure finalization occurs multiple times
-        for (int i = 0; i < NUM_GC_ITERATIONS; ++i) {
+        for (int i = 0; i < numGcIterations; ++i) {
             finalizeSignal = new PhantomReference<>(new Object(), finalizeAwaitQueue);
             Runtime.getRuntime().gc();
             Runtime.getRuntime().runFinalization();
             if (finalizeAwaitQueue.remove(FINALIZE_TIMEOUT_MILLIS) == null) {
                 throw new TimeoutException(
-                        "Finalization failed on iteration " + (i + 1) + " of " + NUM_GC_ITERATIONS);
+                        "Finalization failed on iteration " + (i + 1) + " of " + numGcIterations);
             }
             finalizeSignal.clear();
         }
     }
 
+    /**
+     * Returns a TestRule that runs garbage collection and ensures finalization after each test.
+     */
+    @NonNull
+    public static TestRule getGcRule() {
+        return (base, description) -> new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                base.evaluate();
+                // GC is slow. Doing it once already triples the time for some tests.
+                runFinalization(/* numGcIterations= */1);
+            }
+        };
+    }
+
     // Ensure this utility class can't be instantiated
     private GarbageCollectionUtil() {
     }
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index fee9261..7d19905b 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -1291,8 +1291,8 @@
     method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
     method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.GridItem.Builder setText(CharSequence);
-    method public androidx.car.app.model.GridItem.Builder setTitle(androidx.car.app.model.CarText);
-    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
+    method public androidx.car.app.model.GridItem.Builder setTitle(androidx.car.app.model.CarText?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
   }
 
   @SuppressCompatibility @androidx.car.app.annotations.CarProtocol public final class GridTemplate implements androidx.car.app.model.Template {
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index fee9261..7d19905b 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -1291,8 +1291,8 @@
     method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
     method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.GridItem.Builder setText(CharSequence);
-    method public androidx.car.app.model.GridItem.Builder setTitle(androidx.car.app.model.CarText);
-    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
+    method public androidx.car.app.model.GridItem.Builder setTitle(androidx.car.app.model.CarText?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
   }
 
   @SuppressCompatibility @androidx.car.app.annotations.CarProtocol public final class GridTemplate implements androidx.car.app.model.Template {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index e2cbe97..d34e7b5 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -254,35 +254,33 @@
          * <p>Only {@link DistanceSpan}s and {@link DurationSpan}s are supported in the input
          * string.
          *
-         * @throws NullPointerException     if {@code title} is {@code null}
-         * @throws IllegalArgumentException if {@code title} is empty, of if it contains
-         *                                  unsupported spans
+         * @throws IllegalArgumentException if {@code title} contains unsupported spans
          */
         @NonNull
-        public Builder setTitle(@NonNull CharSequence title) {
-            CarText titleText = CarText.create(requireNonNull(title));
-            if (titleText.isEmpty()) {
-                throw new IllegalArgumentException("The title cannot be null or empty");
+        public Builder setTitle(@Nullable CharSequence title) {
+            if (title == null) {
+                mTitle = null;
+                return this;
             }
+            CarText titleText = CarText.create(title);
             CarTextConstraints.TEXT_ONLY.validateOrThrow(titleText);
             mTitle = titleText;
             return this;
         }
 
         /**
-         * Sets the title of the {@link GridItem}, with support for multiple length variants.,
+         * Sets the title of the {@link GridItem}, with support for multiple length variants.
          *
          * <p>Only {@link DistanceSpan}s and {@link DurationSpan}s are supported in the input
          * string.
          *
-         * @throws NullPointerException     if {@code title} is {@code null}
-         * @throws IllegalArgumentException if {@code title} is empty, of if it contains
-         *                                  unsupported spans
+         * @throws IllegalArgumentException if {@code title} contains unsupported spans
          */
         @NonNull
-        public Builder setTitle(@NonNull CarText title) {
-            if (CarText.isNullOrEmpty(title)) {
-                throw new IllegalArgumentException("The title cannot be null or empty");
+        public Builder setTitle(@Nullable CarText title) {
+            if (title == null) {
+                mTitle = null;
+                return this;
             }
             CarTextConstraints.TEXT_ONLY.validateOrThrow(title);
             mTitle = title;
@@ -432,17 +430,12 @@
         /**
          * Constructs the {@link GridItem} defined by this builder.
          *
-         * @throws IllegalStateException if the grid item's title is not set, if the grid item's
-         *                               image is set when it is loading or vice versa, if
-         *                               the grid item is loading but the click listener is set,
-         *                               or if a badge is set and an image is not set
+         * @throws IllegalStateException if the grid item's image is set when it is loading or vice
+         *                               versa, if the grid item is loading but the click listener
+         *                               is set, or if a badge is set and an image is not set
          */
         @NonNull
         public GridItem build() {
-            if (mTitle == null) {
-                throw new IllegalStateException("A title must be set on the grid item");
-            }
-
             if (mIsLoading == (mImage != null)) {
                 throw new IllegalStateException(
                         "When a grid item is loading, the image must not be set and vice versa");
diff --git a/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
index 6856d45..d3ffeb3 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
@@ -93,18 +93,31 @@
     }
 
     @Test
-    public void title_throwsIfNotSet() {
-        // Not set
-        assertThrows(IllegalStateException.class,
-                () -> new GridItem.Builder().setImage(BACK).build());
-
-        // Not set
-        assertThrows(
-                IllegalArgumentException.class, () -> new GridItem.Builder().setTitle("").setImage(
-                        BACK).build());
+    public void createImage_doesNotThrowIfTitleIsNotSet() {
+        // Test that no exceptions are thrown.
+        GridItem unused = new GridItem.Builder().setImage(BACK).build();
     }
 
     @Test
+    public void title_doesNotThrowIfEmptyString() {
+        // Test that no exceptions are thrown.
+        new GridItem.Builder().setTitle("").setImage(BACK).build();
+    }
+
+    @Test
+    public void title_doesNotThrowIfNullCharSequence() {
+        // Test that no exceptions are thrown.
+        new GridItem.Builder().setTitle((CharSequence) null).setImage(BACK).build();
+    }
+
+    @Test
+    public void title_doesNotThrowIfNullCarText() {
+        // Test that no exceptions are thrown.
+        new GridItem.Builder().setTitle((CarText) null).setImage(BACK).build();
+    }
+
+
+    @Test
     public void text_charSequence() {
         String text = "foo";
         GridItem gridItem = new GridItem.Builder().setTitle("title").setText(text).setImage(
@@ -124,10 +137,18 @@
     }
 
     @Test
-    public void textWithoutTitle_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> new GridItem.Builder().setText("text").setImage(BACK).build());
+    public void textWithoutTitle_returnsNullTitle() {
+        GridItem item = new GridItem.Builder().setText("text").setImage(BACK).build();
+
+        assertThat(item.getTitle()).isNull();
+    }
+
+    @Test
+    public void textSetTitleToNull_returnsNullTitle() {
+        GridItem item = new GridItem.Builder().setTitle("title").setTitle((CharSequence) null)
+                .setImage(BACK).build();
+
+        assertThat(item.getTitle()).isNull();
     }
 
     @Test
diff --git a/compose/animation/animation-core/api/current.ignore b/compose/animation/animation-core/api/current.ignore
index 33ae83f..c707b51 100644
--- a/compose/animation/animation-core/api/current.ignore
+++ b/compose/animation/animation-core/api/current.ignore
@@ -3,3 +3,7 @@
     Method androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig.at has changed return type from androidx.compose.animation.core.KeyframesSpec.KeyframeEntity to androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>
 ChangedType: androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig#atFraction(T, float):
     Method androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig.atFraction has changed return type from androidx.compose.animation.core.KeyframesSpec.KeyframeEntity to androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>
+
+
+RemovedClass: androidx.compose.animation.core.SpringEstimationKt:
+    Removed class androidx.compose.animation.core.SpringEstimationKt
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index 8e1b754..dcd7060 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -569,8 +569,7 @@
     method public S getCurrentState();
     method @FloatRange(from=0.0, to=1.0) public float getFraction();
     method public S getTargetState();
-    method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional S targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public suspend Object? snapTo(S targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? snapTo(optional S targetState, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public S currentState;
     property @FloatRange(from=0.0, to=1.0) public final float fraction;
     property public S targetState;
@@ -597,9 +596,6 @@
     field public static final float StiffnessVeryLow = 50.0f;
   }
 
-  public final class SpringEstimationKt {
-  }
-
   @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public SpringSpec(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
     method public float getDampingRatio();
diff --git a/compose/animation/animation-core/api/restricted_current.ignore b/compose/animation/animation-core/api/restricted_current.ignore
index 33ae83f..c707b51 100644
--- a/compose/animation/animation-core/api/restricted_current.ignore
+++ b/compose/animation/animation-core/api/restricted_current.ignore
@@ -3,3 +3,7 @@
     Method androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig.at has changed return type from androidx.compose.animation.core.KeyframesSpec.KeyframeEntity to androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>
 ChangedType: androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig#atFraction(T, float):
     Method androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig.atFraction has changed return type from androidx.compose.animation.core.KeyframesSpec.KeyframeEntity to androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>
+
+
+RemovedClass: androidx.compose.animation.core.SpringEstimationKt:
+    Removed class androidx.compose.animation.core.SpringEstimationKt
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index cda0a26..2d599a5 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -569,8 +569,7 @@
     method public S getCurrentState();
     method @FloatRange(from=0.0, to=1.0) public float getFraction();
     method public S getTargetState();
-    method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional S targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public suspend Object? snapTo(S targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? snapTo(optional S targetState, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public S currentState;
     property @FloatRange(from=0.0, to=1.0) public final float fraction;
     property public S targetState;
@@ -597,9 +596,6 @@
     field public static final float StiffnessVeryLow = 50.0f;
   }
 
-  public final class SpringEstimationKt {
-  }
-
   @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public SpringSpec(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
     method public float getDampingRatio();
diff --git a/compose/animation/animation-core/lint-baseline.xml b/compose/animation/animation-core/lint-baseline.xml
index 82c2f30..352cbc1 100644
--- a/compose/animation/animation-core/lint-baseline.xml
+++ b/compose/animation/animation-core/lint-baseline.xml
@@ -2,78 +2,6 @@
 <issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
 
     <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="fun &lt;V : AnimationVector> VectorizedAnimationSpec&lt;V>.createAnimation("
-        errorLine2="                                                     ~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/Animation.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="fun estimateAnimationDurationMillis("
-        errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="fun estimateAnimationDurationMillis("
-        errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="fun estimateAnimationDurationMillis("
-        errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="    var playTimeNanos by mutableLongStateOf(0L)"
-        errorLine2="        ~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="    var isSeeking: Boolean by mutableStateOf(false)"
-        errorLine2="        ~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="    inner class DeferredAnimation&lt;T, V : AnimationVector> internal constructor("
-        errorLine2="                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt"/>
-    </issue>
-
-    <issue
-        id="BanSuppressTag"
-        message="@suppress is not allowed in documentation"
-        errorLine1="fun &lt;S, T, V : AnimationVector> Transition&lt;S>.createDeferredAnimation("
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt"/>
-    </issue>
-
-    <issue
         id="PrimitiveInCollection"
         message="constructor VectorizedKeyframesSpec has parameter keyframes with type Map&lt;Integer, ? extends Pair&lt;? extends V, ? extends Easing>>: replace with IntObjectMap"
         errorLine1="        keyframes: Map&lt;Int, Pair&lt;V, Easing>>,"
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
index 0fcbf65..1832f86 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
@@ -61,7 +61,6 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -485,6 +484,7 @@
     Large
 }
 
+@OptIn(ExperimentalTransitionApi::class)
 @Sampled
 @Composable
 fun SeekingAnimationSample() {
@@ -495,53 +495,36 @@
             Row {
                 Button(onClick = {
                     scope.launch { seekingState.animateTo(BoxSize.Small) }
-                },
-                    Modifier
-                        .wrapContentWidth()
-                        .weight(1f)) {
+                }, Modifier.wrapContentWidth().weight(1f)) {
                     Text("Animate Small")
                 }
                 Button(onClick = {
-                    scope.launch { seekingState.seekTo(0f, BoxSize.Small) }
-                },
-                    Modifier
-                        .wrapContentWidth()
-                        .weight(1f)) {
+                    scope.launch { seekingState.snapTo(BoxSize.Small) }
+                }, Modifier.wrapContentWidth().weight(1f)) {
                     Text("Seek Small")
                 }
                 Button(onClick = {
-                    scope.launch { seekingState.seekTo(0f, BoxSize.Medium) }
-                },
-                    Modifier
-                        .wrapContentWidth()
-                        .weight(1f)) {
+                    scope.launch { seekingState.snapTo(BoxSize.Medium) }
+                }, Modifier.wrapContentWidth().weight(1f)) {
                     Text("Seek Medium")
                 }
                 Button(onClick = {
-                    scope.launch { seekingState.seekTo(0f, BoxSize.Large) }
-                },
-                    Modifier
-                        .wrapContentWidth()
-                        .weight(1f)) {
+                    scope.launch { seekingState.snapTo(BoxSize.Large) }
+                }, Modifier.wrapContentWidth().weight(1f)) {
                     Text("Seek Large")
                 }
                 Button(onClick = {
                     scope.launch { seekingState.animateTo(BoxSize.Large) }
-                },
-                    Modifier
-                        .wrapContentWidth()
-                        .weight(1f)) {
+                }, Modifier.wrapContentWidth().weight(1f)) {
                     Text("Animate Large")
                 }
             }
         }
         Slider(
             value = seekingState.fraction,
-            modifier = Modifier
-                .systemGestureExclusion()
-                .padding(10.dp),
+            modifier = Modifier.systemGestureExclusion().padding(10.dp),
             onValueChange = { value ->
-                scope.launch { seekingState.seekTo(fraction = value) }
+                scope.launch { seekingState.snapTo(fraction = value) }
             }
         )
         val transition = rememberTransition(seekingState)
@@ -561,10 +544,7 @@
             fadeIn(tween(easing = LinearEasing)) togetherWith fadeOut(tween(easing = LinearEasing))
         }) { state ->
             if (state == BoxSize.Large) {
-                Box(
-                    Modifier
-                        .size(50.dp)
-                        .background(Color.Magenta))
+                Box(Modifier.size(50.dp).background(Color.Magenta))
             } else {
                 Box(Modifier.size(50.dp))
             }
@@ -582,40 +562,3 @@
         )
     }
 }
-
-@Sampled
-@Composable
-@Suppress("UNUSED_VARIABLE")
-fun SeekToSample() {
-    val seekingState = remember { SeekableTransitionState(BoxSize.Small) }
-    LaunchedEffect(seekingState.targetState) {
-        seekingState.seekTo(0f, BoxSize.Large)
-    }
-    val scope = rememberCoroutineScope()
-    Slider(
-        value = seekingState.fraction,
-        modifier = Modifier
-            .systemGestureExclusion()
-            .padding(10.dp),
-        onValueChange = { value ->
-            scope.launch { seekingState.seekTo(fraction = value) }
-        }
-    )
-    val transition = rememberTransition(seekingState)
-    // use the transition
-}
-
-@Sampled
-@Composable
-@Suppress("UNUSED_VARIABLE")
-fun SnapToSample() {
-    val seekingState = remember { SeekableTransitionState(BoxSize.Small) }
-    val scope = rememberCoroutineScope()
-    Button(onClick = {
-        scope.launch { seekingState.snapTo(BoxSize.Large) }
-    }) {
-        Text("Snap to the Small state")
-    }
-    val transition = rememberTransition(seekingState)
-    // use the transition
-}
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
index 46ba3a1..017fd93 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
@@ -38,6 +39,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
 import kotlinx.coroutines.launch
@@ -49,6 +51,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalTransitionApi::class)
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class SeekableTransitionStateTest {
@@ -68,7 +71,7 @@
 
         rule.setContent {
             LaunchedEffect(seekableTransitionState) {
-                seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
+                seekableTransitionState.snapTo(AnimStates.To)
             }
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             animatedValue = transition.animateInt(
@@ -84,28 +87,28 @@
         rule.runOnIdle {
             assertEquals(0, animatedValue)
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 0.5f)
+                seekableTransitionState.snapTo(fraction = 0.5f)
                 assertEquals(0.5f, seekableTransitionState.fraction)
             }
         }
         rule.runOnIdle {
             assertEquals(500, animatedValue)
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 1f)
+                seekableTransitionState.snapTo(fraction = 1f)
                 assertEquals(1f, seekableTransitionState.fraction)
             }
         }
         rule.runOnIdle {
             assertEquals(1000, animatedValue)
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 0.5f)
+                seekableTransitionState.snapTo(fraction = 0.5f)
                 assertEquals(0.5f, seekableTransitionState.fraction)
             }
         }
         rule.runOnIdle {
             assertEquals(500, animatedValue)
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 0f)
+                seekableTransitionState.snapTo(fraction = 0f)
                 assertEquals(0f, seekableTransitionState.fraction)
             }
         }
@@ -115,19 +118,70 @@
     }
 
     @Test
+    fun changeTarget() {
+        var animatedValue by mutableIntStateOf(-1)
+        var duration by mutableLongStateOf(0)
+        var fromState by mutableStateOf(AnimStates.From)
+        var toState by mutableStateOf(AnimStates.To)
+        lateinit var seekableTransitionState: SeekableTransitionState<AnimStates>
+
+        rule.setContent {
+            seekableTransitionState = remember(fromState, toState) {
+                SeekableTransitionState(fromState)
+            }
+            LaunchedEffect(seekableTransitionState, toState) {
+                seekableTransitionState.snapTo(toState)
+            }
+            val transition = rememberTransition(seekableTransitionState, label = "Test")
+            animatedValue = transition.animateInt(label = "Value") { state ->
+                when (state) {
+                    AnimStates.From -> 0
+                    else -> 1000
+                }
+            }.value
+            duration = transition.totalDurationNanos
+        }
+
+        rule.runOnIdle {
+            assertEquals(0, animatedValue)
+            fromState = AnimStates.To
+            toState = AnimStates.From
+        }
+
+        rule.runOnIdle {
+            assertEquals(1000, animatedValue)
+            runBlocking {
+                seekableTransitionState.snapTo(fraction = 0.5f)
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(animatedValue > 0)
+            assertTrue(animatedValue < 1000)
+            fromState = AnimStates.From
+            toState = AnimStates.To
+        }
+        rule.runOnIdle {
+            assertEquals(0, animatedValue)
+        }
+    }
+
+    @Test
     fun animateToTarget() {
         var animatedValue by mutableIntStateOf(-1)
         var duration by mutableLongStateOf(0)
         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
+        lateinit var coroutineContext: CoroutineContext
         lateinit var coroutineScope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
         rule.setContent {
             LaunchedEffect(seekableTransitionState) {
-                seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
+                seekableTransitionState.snapTo(AnimStates.To)
             }
             coroutineScope = rememberCoroutineScope()
+            coroutineContext = coroutineScope.coroutineContext
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             animatedValue = transition.animateInt(
                 label = "Value",
@@ -141,10 +195,10 @@
             duration = transition.totalDurationNanos
         }
 
-        rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo()
-        val deferred1 = coroutineScope.async {
+        val deferred1 = coroutineScope.async(coroutineContext) {
             seekableTransitionState.animateTo()
         }
+        rule.waitForIdle() // wait for coroutine to run
         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
         rule.mainClock.advanceTimeByFrame()
 
@@ -163,7 +217,7 @@
         // interrupt the progress
 
         runBlocking {
-            seekableTransitionState.seekTo(fraction = 0.5f)
+            seekableTransitionState.snapTo(fraction = 0.5f)
         }
 
         rule.mainClock.advanceTimeByFrame()
@@ -176,7 +230,7 @@
         }
 
         // continue from the same place
-        val deferred2 = coroutineScope.async {
+        val deferred2 = coroutineScope.async(coroutineContext) {
             seekableTransitionState.animateTo()
         }
         rule.waitForIdle() // wait for coroutine to run
@@ -206,7 +260,7 @@
 
         rule.setContent {
             LaunchedEffect(seekableTransitionState) {
-                seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
+                seekableTransitionState.snapTo(AnimStates.To)
             }
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             animatedValue = transition.animateInt(
@@ -236,7 +290,7 @@
 
         runBlocking {
             // Go to the middle
-            seekableTransitionState.seekTo(fraction = 0.5f)
+            seekableTransitionState.snapTo(fraction = 0.5f)
         }
 
         rule.runOnIdle {
@@ -246,7 +300,7 @@
 
         runBlocking {
             // Go to the end
-            seekableTransitionState.seekTo(fraction = 1f)
+            seekableTransitionState.snapTo(fraction = 1f)
         }
 
         rule.runOnIdle {
@@ -256,7 +310,7 @@
 
         runBlocking {
             // Go back to part way through the animatedValue
-            seekableTransitionState.seekTo(fraction = 0.1f)
+            seekableTransitionState.snapTo(fraction = 0.1f)
         }
 
         rule.runOnIdle {
@@ -269,12 +323,17 @@
     fun repeatAnimate() {
         var animatedValue by mutableIntStateOf(-1)
         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
+        lateinit var coroutineContext: CoroutineContext
         lateinit var coroutineScope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
         rule.setContent {
+            LaunchedEffect(seekableTransitionState) {
+                seekableTransitionState.snapTo(AnimStates.To)
+            }
             coroutineScope = rememberCoroutineScope()
+            coroutineContext = coroutineScope.coroutineContext
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             animatedValue = transition.animateInt(
                 label = "Value",
@@ -287,14 +346,15 @@
             }.value
         }
 
-        val deferred1 = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.To)
+        val deferred1 = coroutineScope.async(coroutineContext) {
+            seekableTransitionState.animateTo()
         }
+        rule.waitForIdle() // wait for coroutine to run
         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
         rule.mainClock.advanceTimeByFrame()
 
         // Running the same animation again should cancel the existing one
-        val deferred2 = coroutineScope.async {
+        val deferred2 = coroutineScope.async(coroutineContext) {
             seekableTransitionState.animateTo()
         }
 
@@ -305,8 +365,8 @@
         assertFalse(deferred2.isCancelled)
 
         // seeking should cancel the animation
-        val deferred3 = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0.25f)
+        val deferred3 = coroutineScope.async(coroutineContext) {
+            seekableTransitionState.snapTo(fraction = 0.25f)
         }
 
         rule.waitForIdle() // wait for coroutine to run
@@ -317,7 +377,7 @@
         assertTrue(deferred3.isCompleted)
 
         // start the animation again
-        val deferred4 = coroutineScope.async {
+        val deferred4 = coroutineScope.async(coroutineContext) {
             seekableTransitionState.animateTo()
         }
 
@@ -335,7 +395,7 @@
 
         rule.setContent {
             LaunchedEffect(seekableTransitionState) {
-                seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
+                seekableTransitionState.snapTo(AnimStates.To)
             }
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             animatedValue = transition.animateInt(
@@ -375,7 +435,7 @@
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
             LaunchedEffect(seekableTransitionState) {
-                seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
+                seekableTransitionState.snapTo(AnimStates.To)
             }
             val transition = rememberTransition(seekableTransitionState, label = "Test")
             val val1 = transition.animateInt(
@@ -415,7 +475,7 @@
                         animatedValue3 = val3.value
                     })
         }
-        rule.mainClock.advanceTimeByFrame() // let seekTo() run
+        rule.mainClock.advanceTimeByFrame()
         rule.runOnIdle {
             // Check initial values
             assertEquals(0, animatedValue1)
@@ -423,7 +483,7 @@
             assertEquals(0, animatedValue3)
             // Seek half way
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 0.5f)
+                seekableTransitionState.snapTo(fraction = 0.5f)
                 assertEquals(0.5f, seekableTransitionState.fraction)
             }
         }
@@ -437,11 +497,10 @@
         // Start seek to new state. It won't complete until the initial state is
         // animated to "To"
         val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0f, targetState = AnimStates.Other)
+            seekableTransitionState.snapTo(AnimStates.Other)
         }
-        rule.mainClock.advanceTimeByFrame() // must recompose to Other
+        rule.mainClock.advanceTimeByFrame()
         rule.runOnIdle {
-            assertEquals(AnimStates.Other, seekableTransitionState.targetState)
             // First frame, nothing has changed. We've only gathered the first frame of the
             // animation since it was not previously animating
             assertEquals(500, animatedValue1)
@@ -449,33 +508,28 @@
             assertEquals(500, animatedValue3)
         }
 
-        // Continue the initial value animation. It should use a linear animation.
-        rule.mainClock.advanceTimeBy(80L) // 4 frames of animation
+        // Continue the initial value animation. It should use a spring spec to animate the
+        // value since no animation spec was previously used
+        rule.mainClock.advanceTimeBy(75L)
         rule.runOnIdle {
-            assertEquals(500 + (500f * 80f / 150f), animatedValue1.toFloat(), 1f)
+            assertTrue(animatedValue1 > 500)
+            assertTrue(animatedValue1 < 1000)
             assertEquals(0, animatedValue2)
-            assertEquals(500 + (500f * 80f / 150f), animatedValue3.toFloat(), 1f)
-        }
-        val seekToFraction = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0.5f)
-            assertEquals(0.5f, seekableTransitionState.fraction)
+            assertTrue(animatedValue3 > 500)
+            assertTrue(animatedValue3 < 1000)
+            runBlocking {
+                seekableTransitionState.snapTo(fraction = 0.5f)
+                assertEquals(0.5f, seekableTransitionState.fraction)
+            }
         }
         rule.mainClock.advanceTimeByFrame()
         rule.runOnIdle {
-            val expected1Value = 500 + (500f * 96f / 150f)
-            assertEquals(expected1Value, animatedValue1.toFloat(), 1f)
             assertEquals(500, animatedValue2)
-            assertEquals(
-                expected1Value + 0.5f * (2000 - expected1Value),
-                animatedValue3.toFloat(),
-                1f
-            )
         }
 
         // Advance to the end of the seekTo() animation
         rule.mainClock.advanceTimeBy(5_000)
-        runBlocking { seekToFraction.await() }
-        assertTrue(seekTo.isCancelled)
+        runBlocking { seekTo.await() }
         rule.runOnIdle {
             // The initial values should be 1000/0/1000
             // Target values should be 1000, 1000, 2000
@@ -484,7 +538,7 @@
             assertEquals(500, animatedValue2)
             assertEquals(1500, animatedValue3)
             runBlocking {
-                seekableTransitionState.seekTo(fraction = 1f)
+                seekableTransitionState.snapTo(fraction = 1f)
             }
         }
         rule.mainClock.advanceTimeByFrame()
@@ -571,7 +625,7 @@
 
         val seekTo = coroutineScope.async {
             // seek to Other. This won't finish until the animation finishes
-            seekableTransitionState.seekTo(0f, targetState = AnimStates.Other)
+            seekableTransitionState.snapTo(AnimStates.Other)
         }
 
         rule.runOnIdle {
@@ -589,10 +643,10 @@
             assertEquals(640f, animatedValue1.toFloat(), 1f)
             assertEquals(0, animatedValue2)
             assertEquals(640f, animatedValue3.toFloat(), 1f)
-        }
-        val seekToHalf = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0.5f)
-            assertEquals(0.5f, seekableTransitionState.fraction)
+            runBlocking {
+                seekableTransitionState.snapTo(fraction = 0.5f)
+                assertEquals(0.5f, seekableTransitionState.fraction)
+            }
         }
         rule.runOnIdle {
             assertEquals(500, animatedValue2)
@@ -600,8 +654,7 @@
 
         // Advance to the end of the seekTo() animation
         rule.mainClock.advanceTimeBy(5_000)
-        assertTrue(seekToHalf.isCompleted)
-        assertTrue(seekTo.isCancelled)
+        assertTrue(seekTo.isCompleted)
         rule.runOnIdle {
             // The initial values should be 1000/0/1000
             // Target values should be 1000, 1000, 2000
@@ -609,10 +662,10 @@
             assertEquals(1000, animatedValue1)
             assertEquals(500, animatedValue2)
             assertEquals(1500, animatedValue3)
-        }
-        coroutineScope.launch {
-            seekableTransitionState.seekTo(fraction = 1f)
-            assertEquals(1f, seekableTransitionState.fraction, 0f)
+            runBlocking {
+                seekableTransitionState.snapTo(fraction = 1f)
+                assertEquals(1f, seekableTransitionState.fraction, 0f)
+            }
         }
         rule.mainClock.advanceTimeByFrame()
         rule.runOnIdle {
@@ -676,6 +729,7 @@
                         animatedValue3 = val3.value
                     })
         }
+        rule.waitForIdle() // let the launched effect start
         rule.mainClock.advanceTimeByFrame() // lock in the animation start time
         rule.runOnIdle {
             assertEquals(0f, seekableTransitionState.fraction, 0.01f)
@@ -699,8 +753,16 @@
             seekableTransitionState.animateTo(AnimStates.Other)
         }
 
-        rule.mainClock.advanceTimeBy(16) // composition after animateTo()
+        rule.runOnIdle {
+            // Nothing will have changed yet. The initial value should continue to animate
+            // after this
+            assertEquals(533f, animatedValue1.toFloat(), 1f)
+            assertEquals(0, animatedValue2)
+            assertEquals(533f, animatedValue3.toFloat(), 1f)
+        }
 
+        // Lock in the animation for the animation to Other, but advance animation to To
+        rule.mainClock.advanceTimeBy(16)
         rule.runOnIdle {
             // initial should be 176/300 = 0.587 through animation
             assertEquals(586.7f, animatedValue1.toFloat(), 1f)
@@ -708,24 +770,14 @@
             assertEquals(586.7f, animatedValue3.toFloat(), 1f)
         }
 
-        // Lock in the animation for the animation to Other, but advance animation to To
-        rule.mainClock.advanceTimeBy(16)
-        rule.runOnIdle {
-            // initial should be 192/300 = 0.640 through animation
-            // target should be 16/300 = 0.053
-            assertEquals(640f, animatedValue1.toFloat(), 1f)
-            assertEquals(53.3f, animatedValue2.toFloat(), 1f)
-            assertEquals(640f + ((2000f - 640f) * 0.053f), animatedValue3.toFloat(), 1f)
-        }
-
         // Advance time by two more frames
         rule.mainClock.advanceTimeBy(32)
         rule.runOnIdle {
-            // initial should be 224/300 = 0.746.7 through animation
-            // other should be 48/300 = 0.160 through the animation
-            assertEquals(746.7f, animatedValue1.toFloat(), 1f)
-            assertEquals(160f, animatedValue2.toFloat(), 1f)
-            assertEquals(746.7f + ((2000f - 746.7f) * 0.160f), animatedValue3.toFloat(), 2f)
+            // initial should be 208/300 = 0.693 through animation
+            // other should be 32/300 = 0.107 through the animation
+            assertEquals(693f, animatedValue1.toFloat(), 1f)
+            assertEquals(107f, animatedValue2.toFloat(), 1f)
+            assertEquals(693f + ((2000f - 693f) * 0.107f), animatedValue3.toFloat(), 2f)
         }
 
         // Advance to the end of the animation
@@ -824,22 +876,45 @@
             assertEquals(586, animatedValue3)
         }
 
-        // Compose the change
+        // Lock in the animation for initial animation and target animation
         rule.mainClock.advanceTimeBy(16)
 
         rule.runOnIdle {
-            // The previous animation's start time can be used, so continue the animation
-            assertEquals(640, animatedValue1)
-            assertEquals(0, animatedValue2) // animation hasn't started yet
-            assertEquals(640, animatedValue3) // animation hasn't started yet
+            // Nothing will have changed yet, but both animations will start on the next frame
+            assertEquals(586, animatedValue1)
+            assertEquals(0, animatedValue2)
+            assertEquals(586, animatedValue3)
         }
 
+        var previousAnimatedValue1 = 586
+
         // Advance one frame
         rule.mainClock.advanceTimeBy(16)
         rule.runOnIdle {
-            assertEquals(693, animatedValue1)
+            // initial value should be animated by a spring at this point
+            // target animation should be 16/300 = 0.05333
+            assertTrue(animatedValue1 > previousAnimatedValue1)
             assertEquals(53, animatedValue2)
-            assertEquals(693 + ((2000 - 693) * 16 / 300), animatedValue3)
+            assertEquals(
+                animatedValue1 + (0.053333f * (2000f - animatedValue1)),
+                animatedValue3.toFloat(),
+                2f
+            )
+            previousAnimatedValue1 = animatedValue1
+        }
+
+        // Advance time by two more frames
+        rule.mainClock.advanceTimeBy(32)
+        rule.runOnIdle {
+            // initial should continue to be animated by a spring
+            // other should be 48/300 = 0.160 through the animation
+            assertTrue(animatedValue1 > previousAnimatedValue1)
+            assertEquals(160, animatedValue2)
+            assertEquals(
+                animatedValue1 + ((2000f - animatedValue1) * 0.16f),
+                animatedValue3.toFloat(),
+                2f
+            )
         }
 
         // Advance to the end of the animation
@@ -901,16 +976,16 @@
                     })
         }
         rule.waitForIdle()
-        coroutineScope.launch { seekableTransitionState.seekTo(0f, targetState = AnimStates.To) }
+        coroutineScope.launch { seekableTransitionState.snapTo(AnimStates.To) }
         rule.waitForIdle()
-        coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) }
+        coroutineScope.launch { seekableTransitionState.snapTo(fraction = 0.5f) }
         rule.waitForIdle()
-        coroutineScope.launch { seekableTransitionState.seekTo(0f, targetState = AnimStates.Other) }
+        coroutineScope.launch { seekableTransitionState.snapTo(AnimStates.Other) }
         rule.waitForIdle()
         rule.mainClock.advanceTimeByFrame() // lock in the initial value animation start time
-        coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) }
+        coroutineScope.launch { seekableTransitionState.snapTo(fraction = 0.5f) }
         rule.waitForIdle()
-        coroutineScope.launch { seekableTransitionState.seekTo(0f, targetState = AnimStates.From) }
+        coroutineScope.launch { seekableTransitionState.snapTo(AnimStates.From) }
         rule.waitForIdle()
 
         // Now we have two initial value animations running. One is for animating
@@ -951,12 +1026,12 @@
             }
         }
         rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0.5f, AnimStates.To)
+        coroutineScope.launch {
+            seekableTransitionState.snapTo(AnimStates.To, 0.5f)
         }
-        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame() // must wait for a composition and draw
         rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
             assertEquals(150L * MillisToNanos, transition.playTimeNanos)
         }
     }
@@ -995,7 +1070,7 @@
         assertEquals(1000, animatedValue1)
 
         // seeking after the animation has completed should not change any value
-        coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) }
+        coroutineScope.launch { seekableTransitionState.snapTo(fraction = 0.5f) }
         rule.waitForIdle()
         rule.mainClock.advanceTimeByFrame()
         assertEquals(1000, animatedValue1)
@@ -1029,7 +1104,7 @@
         }
         rule.waitForIdle()
         coroutineScope.launch {
-            seekableTransitionState.seekTo(0.5f, AnimStates.To)
+            seekableTransitionState.snapTo(AnimStates.To, 0.5f)
         }
         rule.waitForIdle()
         rule.mainClock.advanceTimeByFrame()
@@ -1058,1238 +1133,4 @@
             assertEquals(1000, animatedValue1)
         }
     }
-
-    @Test
-    fun seekToFollowedByAnimation() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.seekTo(1f, AnimStates.To)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
-        rule.runOnIdle { // seekTo() should run now, setting the animated value
-            assertEquals(1000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the animation clock
-        rule.runOnIdle {
-            assertEquals(1000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(984, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(968, animatedValue1)
-        }
-        rule.mainClock.advanceTimeBy(1000)
-        rule.runOnIdle {
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun conflictingSeekTo() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val defer1 = coroutineScope.async {
-            seekableTransitionState.seekTo(1f, AnimStates.To)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        val defer2 = coroutineScope.async {
-            seekableTransitionState.seekTo(1f, AnimStates.Other)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
-        rule.runOnIdle {
-            assertTrue(defer1.isCancelled)
-            assertFalse(defer2.isCancelled)
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the animation clock
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(1968, animatedValue1)
-        }
-        rule.mainClock.advanceTimeBy(1000)
-        rule.runOnIdle {
-            assertEquals(0, animatedValue1)
-            assertTrue(defer2.isCompleted)
-        }
-    }
-
-    @Test
-    fun conflictingSnapTo() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val defer1 = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.To)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        val defer2 = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.Other)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
-        rule.runOnIdle {
-            assertTrue(defer1.isCancelled)
-            assertFalse(defer2.isCancelled)
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the animation clock
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(1968, animatedValue1)
-        }
-        rule.mainClock.advanceTimeBy(1000)
-        rule.runOnIdle {
-            assertEquals(0, animatedValue1)
-            assertTrue(defer2.isCompleted)
-        }
-    }
-
-    /**
-     * Here, the first seekTo() doesn't do anything since the target is the same as the current
-     * value. It only changes the fraction.
-     */
-    @Test
-    fun conflictingSeekTo2() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.seekTo(1f, AnimStates.From)
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        coroutineScope.launch {
-            seekableTransitionState.seekTo(1f, AnimStates.Other)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the animation clock
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(1968, animatedValue1)
-        }
-        rule.mainClock.advanceTimeBy(1000)
-        rule.runOnIdle {
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    /**
-     * Here, the first seekTo() doesn't do anything since the target is the same as the current
-     * value. It only changes the fraction.
-     */
-    @Test
-    fun conflictingSnapTo2() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.snapTo(AnimStates.From)
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        coroutineScope.launch {
-            seekableTransitionState.snapTo(AnimStates.Other)
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // let the composition happen after snapTo
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the animation clock
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(1968, animatedValue1)
-        }
-        rule.mainClock.advanceTimeBy(1000)
-        rule.runOnIdle {
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun snapToStopsAllAnimations() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.seekTo(1f, AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-        val animation = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.Other)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-        val snapTo = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(animation.isCancelled)
-            assertTrue(snapTo.isCompleted)
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun snapToSameTargetState() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0.5f, AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val snapTo = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(snapTo.isCompleted)
-            assertEquals(1000, animatedValue1)
-        }
-    }
-
-    @Test
-    fun snapToSameCurrentState() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0.5f, AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val snapTo = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(snapTo.isCompleted)
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun snapToExistingState() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val snapTo = coroutineScope.async {
-            seekableTransitionState.snapTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertTrue(snapTo.isCompleted)
-            assertEquals(0, animatedValue1)
-        }
-        val seekAndSnap = coroutineScope.async {
-            seekableTransitionState.seekTo(0.5f, AnimStates.To)
-            seekableTransitionState.snapTo(AnimStates.From)
-            seekableTransitionState.snapTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // seekTo
-        rule.mainClock.advanceTimeByFrame() // snapTo
-        rule.runOnIdle {
-            assertTrue(seekAndSnap.isCompleted)
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun animateAndContinueAnimation() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val animateTo = coroutineScope.async {
-            seekableTransitionState.animateTo()
-        }
-        rule.mainClock.advanceTimeByFrame() // lock animation clock
-        rule.mainClock.advanceTimeBy(160)
-        rule.runOnIdle {
-            assertEquals(160, animatedValue1)
-        }
-
-        val animateTo2 = coroutineScope.async {
-            seekableTransitionState.animateTo()
-        }
-
-        rule.runOnIdle {
-            assertTrue(animateTo.isCancelled)
-        }
-
-        rule.mainClock.advanceTimeByFrame() // continue the animation
-
-        rule.runOnIdle {
-            assertEquals(176, animatedValue1)
-        }
-
-        rule.mainClock.advanceTimeBy(900)
-
-        rule.runOnIdle {
-            assertTrue(animateTo2.isCompleted)
-            assertEquals(1000, animatedValue1)
-        }
-    }
-
-    @Test
-    fun continueAnimationWithNewSpec() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val animateTo = coroutineScope.async {
-            seekableTransitionState.animateTo()
-        }
-        rule.mainClock.advanceTimeByFrame() // lock animation clock
-        rule.mainClock.advanceTimeBy(160)
-        rule.runOnIdle {
-            assertEquals(160, animatedValue1)
-        }
-
-        val animateTo2 = coroutineScope.async {
-            seekableTransitionState.animateTo(
-                animationSpec = tween(
-                    durationMillis = 200,
-                    easing = LinearEasing
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertTrue(animateTo.isCancelled)
-        }
-
-        rule.mainClock.advanceTimeByFrame() // continue the animation
-
-        rule.runOnIdle {
-            // 160 + (840 * 16/200) = 227.2
-            assertEquals(227.2f, animatedValue1.toFloat(), 1f)
-        }
-
-        rule.mainClock.advanceTimeBy(200)
-
-        rule.runOnIdle {
-            assertTrue(animateTo2.isCompleted)
-            assertEquals(1000, animatedValue1)
-        }
-    }
-
-    @Test
-    fun continueAnimationUsesInitialVelocity() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val animateTo = coroutineScope.async {
-            seekableTransitionState.animateTo()
-        }
-        rule.mainClock.advanceTimeByFrame() // lock animation clock
-        rule.mainClock.advanceTimeBy(800) // half way
-        rule.runOnIdle {
-            assertEquals(500, animatedValue1)
-        }
-
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(
-                animationSpec = spring(
-                    visibilityThreshold = 0.01f,
-                    stiffness = Spring.StiffnessVeryLow
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertTrue(animateTo.isCancelled)
-        }
-
-        rule.mainClock.advanceTimeByFrame() // continue the animation
-
-        rule.runOnIdle {
-            // The velocity should be similar to what it was before after only one frame
-            // 500 / 800 = 0.625 pixels per ms * 16 = 10 pixels
-            assertEquals(510f, animatedValue1.toFloat(), 2f)
-        }
-    }
-
-    @Test
-    fun continueAnimationNewSpecUsesInitialVelocity() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-        }
-        val springSpec = spring<Float>(dampingRatio = 2f)
-        val vecSpringSpec = springSpec.vectorize(Float.VectorConverter)
-        val animateTo = coroutineScope.async {
-            seekableTransitionState.animateTo(animationSpec = springSpec)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock animation clock
-
-        // find how long it takes to get to about half way:
-        var halfDuration = 16L
-        val zeroVector = AnimationVector1D(0f)
-        val oneVector = AnimationVector1D(1f)
-        while (vecSpringSpec.getValueFromMillis(
-            playTimeMillis = halfDuration,
-            start = zeroVector,
-            end = oneVector,
-            startVelocity = zeroVector
-        )[0] < 0.5f) {
-            halfDuration += 16L
-        }
-        rule.mainClock.advanceTimeBy(halfDuration) // ~half way
-        val halfValue = vecSpringSpec.getValueFromMillis(
-            playTimeMillis = halfDuration,
-            start = zeroVector,
-            end = oneVector,
-            startVelocity = zeroVector
-        )[0] * 1000
-        rule.runOnIdle {
-            assertEquals(halfValue, animatedValue1.toFloat(), 1f)
-        }
-
-        val velocityAtHalfWay = vecSpringSpec.getVelocityFromNanos(
-            playTimeNanos = halfDuration * MillisToNanos,
-            initialValue = zeroVector,
-            targetValue = oneVector,
-            initialVelocity = zeroVector
-        )[0]
-
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(
-                animationSpec = spring(
-                    visibilityThreshold = 0.01f,
-                    stiffness = Spring.StiffnessVeryLow,
-                    dampingRatio = Spring.DampingRatioHighBouncy
-                )
-            )
-        }
-
-        rule.runOnIdle {
-            assertTrue(animateTo.isCancelled)
-        }
-
-        rule.mainClock.advanceTimeByFrame() // continue the animation
-
-        rule.runOnIdle {
-            // The velocity should be similar to what it was before after only one frame
-            assertEquals(halfValue + (velocityAtHalfWay * 16f), animatedValue1.toFloat(), 2f)
-        }
-    }
-
-    @Test
-    fun animationCompletionHasNoInitialValueAnimation() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                val target = when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-                target
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        rule.mainClock.advanceTimeBy(1700)
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(AnimStates.From)
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the clock
-        rule.runOnIdle {
-            assertEquals(1000, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(990, animatedValue1)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(980, animatedValue1)
-        }
-    }
-
-    @Test
-    fun animationDurationWorksOnInitialStateChange() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        var animatedValue2 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.Other -> 2000
-                    else -> 1000
-                }
-            }
-            val val2 = transition.animateInt(
-                label = "Value2",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                        animatedValue2 = val2.value
-                    })
-        }
-        rule.waitForIdle()
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(
-                AnimStates.To,
-                animationSpec = tween(durationMillis = 160, easing = LinearEasing)
-            )
-        }
-        rule.mainClock.advanceTimeByFrame() // lock in the clock
-        coroutineScope.launch {
-            seekableTransitionState.animateTo(AnimStates.Other)
-        }
-        rule.mainClock.advanceTimeByFrame() // advance one frame toward To and compose to Other
-        rule.runOnIdle {
-            assertEquals(100, animatedValue1)
-            assertEquals(100, animatedValue2)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            // 200 + (1800 * 16/1600) = 218
-            assertEquals(218, animatedValue1)
-            // continue the animatedValue2 animation
-            assertEquals(200, animatedValue2)
-        }
-        rule.mainClock.advanceTimeBy(128)
-        rule.runOnIdle {
-            // 1000 + (1000 * 144/1600) = 1090
-            assertEquals(1090, animatedValue1)
-            assertEquals(1000, animatedValue2)
-        }
-        rule.mainClock.advanceTimeBy(1600)
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-            assertEquals(1000, animatedValue2)
-        }
-    }
-
-    @Test
-    fun animationAlreadyAtTarget() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(1f, AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-            assertEquals(1000, animatedValue1)
-        }
-        val anim = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // compose to current state = target state
-        rule.runOnIdle {
-            assertTrue(anim.isCompleted)
-            assertEquals(AnimStates.To, seekableTransitionState.currentState)
-            assertEquals(AnimStates.To, seekableTransitionState.targetState)
-        }
-    }
-
-    @Test
-    fun seekCurrentEqualsTarget() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0.5f)
-        }
-        rule.runOnIdle {
-            assertTrue(seekTo.isCompleted)
-            assertEquals(0, animatedValue1)
-        }
-    }
-
-    @Test
-    fun animateToAtEndCurrentInitialValueAnimations() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        var animatedValue2 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.To -> 1000
-                    AnimStates.Other -> 2000
-                }
-            }
-            val val2 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                        animatedValue2 = val2.value
-                    })
-        }
-        rule.waitForIdle()
-        val seekTo = coroutineScope.async {
-            seekableTransitionState.seekTo(0f, AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // compose to To
-        assertTrue(seekTo.isCompleted)
-        val seekOther = coroutineScope.async {
-            seekableTransitionState.seekTo(1f, AnimStates.Other)
-        }
-        rule.mainClock.advanceTimeByFrame() // compose to Other
-        assertFalse(seekOther.isCompleted) // should be animating animatedValue2
-        val animateOther = coroutineScope.async {
-            // already at the end (1f), but it should continue the animatedValue2 animation
-            seekableTransitionState.animateTo(AnimStates.Other)
-        }
-        assertTrue(seekOther.isCancelled)
-        assertTrue(animateOther.isActive)
-        rule.mainClock.advanceTimeByFrame() // advance the animation
-        rule.runOnIdle {
-            assertTrue(animateOther.isActive)
-            assertEquals(2000, animatedValue1)
-            assertEquals(1000 * 16 / 1600, animatedValue2)
-        }
-    }
-
-    @Test
-    fun changingDuration() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        var animatedValue2 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    else -> 1000
-                }
-            }
-            val val2 = if (val1.value < 500) {
-                mutableIntStateOf(0)
-            } else {
-                transition.animateInt(
-                    label = "Value2",
-                    transitionSpec = { tween(durationMillis = 3200, easing = LinearEasing) }
-                ) { state ->
-                    when (state) {
-                        AnimStates.From -> 0
-                        else -> 1000
-                    }
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                        animatedValue2 = val2.value
-                    })
-        }
-        rule.waitForIdle()
-        val anim = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition
-        rule.mainClock.advanceTimeBy(800) // half way through
-        rule.runOnIdle {
-            assertEquals(500, animatedValue1)
-            assertEquals(0, animatedValue2)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            assertEquals(510, animatedValue1)
-            assertEquals(5, animatedValue2)
-        }
-        rule.mainClock.advanceTimeBy(784)
-        rule.runOnIdle {
-            assertEquals(1000, animatedValue1)
-            assertEquals(250, animatedValue2)
-            assertFalse(anim.isCompleted)
-        }
-        rule.mainClock.advanceTimeBy(2400)
-        rule.runOnIdle {
-            assertEquals(1000, animatedValue1)
-            assertEquals(1000, animatedValue2)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition
-        assertTrue(anim.isCompleted)
-    }
-
-    @Test
-    fun changingAnimationWithAnimateToThirdState() {
-        rule.mainClock.autoAdvance = false
-        val seekableTransitionState = SeekableTransitionState(AnimStates.From)
-        var animatedValue1 by mutableIntStateOf(-1)
-        var animatedValue2 by mutableIntStateOf(-1)
-        lateinit var coroutineScope: CoroutineScope
-
-        rule.setContent {
-            coroutineScope = rememberCoroutineScope()
-            val transition = rememberTransition(seekableTransitionState, label = "Test")
-            val val1 = transition.animateInt(
-                label = "Value",
-                transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
-            ) { state ->
-                when (state) {
-                    AnimStates.From -> 0
-                    AnimStates.To -> 1000
-                    else -> 2000
-                }
-            }
-            val val2 = if (val1.value < 500) {
-                mutableIntStateOf(0)
-            } else {
-                transition.animateInt(
-                    label = "Value2",
-                    transitionSpec = { tween(durationMillis = 3200, easing = LinearEasing) }
-                ) { state ->
-                    when (state) {
-                        AnimStates.From -> 0
-                        AnimStates.To -> 1000
-                        else -> 2000
-                    }
-                }
-            }
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .drawBehind {
-                        animatedValue1 = val1.value
-                        animatedValue2 = val2.value
-                    })
-        }
-        rule.waitForIdle()
-        val animateTo = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.To)
-        }
-        rule.mainClock.advanceTimeByFrame() // wait for composition
-        rule.mainClock.advanceTimeBy(800) // half way through
-
-        rule.runOnIdle {
-            // Won't have advanced the value to Other, but will continue advance to To
-            assertEquals(1000 * 800 / 1600, animatedValue1)
-            assertEquals(1000 * 0 / 3200, animatedValue2)
-        }
-        rule.mainClock.advanceTimeByFrame() // one frame past recomposition, so animation is running
-
-        rule.runOnIdle {
-            // Won't have advanced the value to Other, but will continue advance to To
-            assertEquals(1000 * 816 / 1600, animatedValue1)
-            assertEquals(1000 * 16 / 3200, animatedValue2)
-        }
-
-        // now seek to third state
-        val animateOther = coroutineScope.async {
-            seekableTransitionState.animateTo(AnimStates.Other)
-        }
-        assertTrue(animateTo.isCancelled)
-        rule.mainClock.advanceTimeByFrame() // wait for composition
-        rule.runOnIdle {
-            // Won't have advanced the value to Other, but will continue advance to To
-            assertEquals(1000 * 832 / 1600, animatedValue1)
-            assertEquals(1000 * 32 / 3200, animatedValue2)
-        }
-        rule.mainClock.advanceTimeByFrame()
-        rule.runOnIdle {
-            // Continues the advance to Other
-            val anim1Value1 = 1000 * 848 / 1600
-            val anim1Value2 = 1000 * 48 / 3200
-            assertEquals(anim1Value1 + ((2000 - anim1Value1) * 16 / 1600), animatedValue1)
-            assertEquals(anim1Value2 + ((2000 - anim1Value2) * 16 / 3200), animatedValue2)
-        }
-
-        rule.mainClock.advanceTimeBy(752)
-        rule.runOnIdle {
-            val anim1Value1 = 1000
-            val anim1Value2 = 1000 * 800 / 3200
-            assertEquals(anim1Value1 + ((2000 - anim1Value1) * 768 / 1600), animatedValue1)
-            assertEquals(anim1Value2 + ((2000 - anim1Value2) * 768 / 3200), animatedValue2)
-            assertFalse(animateOther.isCompleted)
-        }
-
-        rule.mainClock.advanceTimeBy(832)
-        rule.runOnIdle {
-            val anim1Value2 = 1000 * 1632 / 3200
-            assertEquals(2000, animatedValue1)
-            assertEquals(anim1Value2 + ((2000 - anim1Value2) * 1600 / 3200), animatedValue2)
-            assertFalse(animateOther.isCompleted)
-        }
-
-        rule.mainClock.advanceTimeBy(1600)
-        rule.runOnIdle {
-            assertEquals(2000, animatedValue1)
-            assertEquals(2000, animatedValue2)
-            assertFalse(animateOther.isCompleted)
-        }
-
-        rule.mainClock.advanceTimeByFrame() // composition after the current value changes
-        rule.runOnIdle {
-            assertTrue(animateOther.isCompleted)
-        }
-    }
 }
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
deleted file mode 100644
index efabf6c..0000000
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.core
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class DelayedAnimationTest {
-    @Test
-    fun duration() {
-        val tweenSpec = tween<Float>(1000, easing = LinearEasing)
-        val delayed = delayed(tweenSpec, 500L * MillisToNanos)
-
-        val vectorizedSpec = delayed.vectorize(Float.VectorConverter)
-
-        val initialValue = AnimationVector1D(0f)
-        val targetValue = AnimationVector1D(1000f)
-        val initialVelocity = AnimationVector1D(30f)
-
-        assertThat(vectorizedSpec.getDurationNanos(
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )).isEqualTo(1500L * MillisToNanos)
-    }
-
-    @Test
-    fun values() {
-        val delayNanos = 500L * MillisToNanos
-        val tweenSpec = tween<Float>(1000, easing = LinearEasing)
-        val delayed = delayed(tweenSpec, delayNanos)
-
-        val vectorizedSpec = delayed.vectorize(Float.VectorConverter)
-
-        val initialValue = AnimationVector1D(0f)
-        val targetValue = AnimationVector1D(1000f)
-        val initialVelocity = AnimationVector1D(30f)
-
-        assertThat(vectorizedSpec.getValueFromNanos(
-            playTimeNanos = 0L,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(0f)
-
-        assertThat(vectorizedSpec.getValueFromNanos(
-            playTimeNanos = delayNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(0f)
-
-        assertThat(vectorizedSpec.getValueFromNanos(
-            playTimeNanos = 1000L * MillisToNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(500f)
-
-        assertThat(vectorizedSpec.getValueFromNanos(
-            playTimeNanos = 1500L * MillisToNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(1000f)
-    }
-
-    @Test
-    fun velocity() {
-        val delayNanos = 500L * MillisToNanos
-        val springSpec = spring<Float>()
-        val delayed = delayed(springSpec, delayNanos)
-
-        val vectorizedSpec = delayed.vectorize(Float.VectorConverter)
-
-        val initialValue = AnimationVector1D(0f)
-        val targetValue = AnimationVector1D(1000f)
-        val initialVelocity = AnimationVector1D(30f)
-
-        assertThat(vectorizedSpec.getVelocityFromNanos(
-            playTimeNanos = 0L,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(30f)
-
-        assertThat(vectorizedSpec.getVelocityFromNanos(
-            playTimeNanos = delayNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(30f)
-
-        val vectorizedSpringSpec = springSpec.vectorize(Float.VectorConverter)
-
-        val springDuration = vectorizedSpringSpec.getDurationNanos(
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )
-
-        assertThat(vectorizedSpec.getVelocityFromNanos(
-            playTimeNanos = (delayNanos) + (springDuration / 5),
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(
-            vectorizedSpringSpec.getVelocityFromNanos(
-                playTimeNanos = springDuration / 5,
-                initialValue = initialValue,
-                targetValue = targetValue,
-                initialVelocity = initialVelocity
-            )[0]
-        )
-
-        assertThat(vectorizedSpec.getVelocityFromNanos(
-            playTimeNanos = (delayNanos) + (springDuration / 2),
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(
-            vectorizedSpringSpec.getVelocityFromNanos(
-                playTimeNanos = springDuration / 2,
-                initialValue = initialValue,
-                targetValue = targetValue,
-                initialVelocity = initialVelocity
-            )[0]
-        )
-
-        assertThat(vectorizedSpec.getVelocityFromNanos(
-            playTimeNanos = (delayNanos) + springDuration,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )[0]).isEqualTo(
-            vectorizedSpringSpec.getVelocityFromNanos(
-                playTimeNanos = springDuration,
-                initialValue = initialValue,
-                targetValue = targetValue,
-                initialVelocity = initialVelocity
-            )[0]
-        )
-    }
-
-    @Test
-    fun zeroAnimation() {
-        val springSpec = spring<Float>()
-        val delayed = delayed(springSpec, 0L)
-
-        val vectorizedSpec = delayed.vectorize(Float.VectorConverter)
-
-        val initialValue = AnimationVector1D(1f)
-        val targetValue = AnimationVector1D(1f)
-        val initialVelocity = AnimationVector1D(0f)
-
-        assertThat(vectorizedSpec.getDurationNanos(
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )).isEqualTo(0L)
-    }
-
-    @Test
-    fun comparison() {
-        val tweenSpec = tween<Float>(1000, easing = LinearEasing)
-        val delayed1 = delayed(tweenSpec, 500L * MillisToNanos)
-        val delayed2 = delayed(tweenSpec, 500L * MillisToNanos)
-        val delayed3 = delayed(tweenSpec, 400L * MillisToNanos)
-        val delayed4 = delayed(spring<Float>(), 400L * MillisToNanos)
-        assertThat(delayed1).isEqualTo(delayed2)
-        assertThat(delayed1).isNotEqualTo(delayed3)
-        assertThat(delayed1).isNotEqualTo(delayed4)
-
-        assertThat(delayed1.hashCode()).isEqualTo(delayed2.hashCode())
-        assertThat(delayed1.hashCode()).isNotEqualTo(delayed3.hashCode())
-        assertThat(delayed1.hashCode()).isNotEqualTo(delayed4.hashCode())
-    }
-}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animation.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animation.kt
index 0807442..58badba 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animation.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animation.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.animation.core
 
+import androidx.annotation.RestrictTo
+
 /**
  * This interface provides a convenient way to query from an [VectorizedAnimationSpec] or
  * [FloatDecayAnimationSpec]: It spares the need to pass the starting conditions and in some cases
@@ -107,8 +109,8 @@
  * @param initialValue the value that the animation will start from
  * @param targetValue the value that the animation will end at
  * @param initialVelocity the initial velocity to start the animation at
- * @suppress
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 fun <V : AnimationVector> VectorizedAnimationSpec<V>.createAnimation(
     initialValue: V,
     targetValue: V,
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
index 30e49d0..0853604 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
@@ -922,107 +922,3 @@
  */
 @Stable
 fun <T> snap(delayMillis: Int = 0) = SnapSpec<T>(delayMillis)
-
-/**
- * Returns an [AnimationSpec] that is the same as [animationSpec] with a delay of [startDelayNanos].
- */
-@Stable
-internal fun <T> delayed(
-    animationSpec: AnimationSpec<T>,
-    startDelayNanos: Long
-): AnimationSpec<T> = StartDelayAnimationSpec(animationSpec, startDelayNanos)
-
-/**
- * A [VectorizedAnimationSpec] that wraps [vectorizedAnimationSpec], giving it a start delay
- * of [startDelayNanos].
- */
-@Immutable
-private class StartDelayVectorizedAnimationSpec<V : AnimationVector>(
-    val vectorizedAnimationSpec: VectorizedAnimationSpec<V>,
-    val startDelayNanos: Long
-) : VectorizedAnimationSpec<V> {
-    override val isInfinite: Boolean
-        get() = vectorizedAnimationSpec.isInfinite
-
-    override fun getDurationNanos(
-        initialValue: V,
-        targetValue: V,
-        initialVelocity: V
-    ): Long = vectorizedAnimationSpec.getDurationNanos(
-        initialValue = initialValue,
-        targetValue = targetValue,
-        initialVelocity = initialVelocity
-    ) + startDelayNanos
-
-    override fun getVelocityFromNanos(
-        playTimeNanos: Long,
-        initialValue: V,
-        targetValue: V,
-        initialVelocity: V
-    ): V = if (playTimeNanos < startDelayNanos) {
-        initialVelocity
-    } else {
-        vectorizedAnimationSpec.getVelocityFromNanos(
-            playTimeNanos = playTimeNanos - startDelayNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )
-    }
-
-    override fun getValueFromNanos(
-        playTimeNanos: Long,
-        initialValue: V,
-        targetValue: V,
-        initialVelocity: V
-    ): V = if (playTimeNanos < startDelayNanos) {
-        initialValue
-    } else {
-        vectorizedAnimationSpec.getValueFromNanos(
-            playTimeNanos = playTimeNanos - startDelayNanos,
-            initialValue = initialValue,
-            targetValue = targetValue,
-            initialVelocity = initialVelocity
-        )
-    }
-
-    override fun hashCode(): Int {
-        return 31 * vectorizedAnimationSpec.hashCode() + startDelayNanos.hashCode()
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (other !is StartDelayVectorizedAnimationSpec<*>) {
-            return false
-        }
-        return other.startDelayNanos == startDelayNanos &&
-            other.vectorizedAnimationSpec == vectorizedAnimationSpec
-    }
-}
-
-/**
- * An [AnimationSpec] that wraps [animationSpec], giving it a start delay of [startDelayNanos].
- */
-@Immutable
-private class StartDelayAnimationSpec<T>(
-    val animationSpec: AnimationSpec<T>,
-    val startDelayNanos: Long
-) : AnimationSpec<T> {
-    override fun <V : AnimationVector> vectorize(
-        converter: TwoWayConverter<T, V>
-    ): VectorizedAnimationSpec<V> {
-        val vecSpec = animationSpec.vectorize(converter)
-        return StartDelayVectorizedAnimationSpec(vecSpec, startDelayNanos)
-    }
-
-    override fun hashCode(): Int {
-        return 31 * animationSpec.hashCode() + startDelayNanos.hashCode()
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (other !is StartDelayAnimationSpec<*>) {
-            return false
-        }
-        return other.startDelayNanos == startDelayNanos &&
-            other.animationSpec == animationSpec
-    }
-}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
index fe55eea..d0e40c0 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.animation.core
 
+import androidx.annotation.RestrictTo
 import kotlin.math.abs
 import kotlin.math.exp
 import kotlin.math.ln
@@ -31,8 +32,8 @@
 
 /**
  * Returns the estimated time that the spring will last be at [delta]
- * @suppress
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 fun estimateAnimationDurationMillis(
     stiffness: Float,
     dampingRatio: Float,
@@ -56,8 +57,8 @@
 
 /**
  * Returns the estimated time that the spring will last be at [delta]
- * @suppress
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 fun estimateAnimationDurationMillis(
     stiffness: Double,
     dampingRatio: Double,
@@ -85,8 +86,8 @@
 
 /**
  * Returns the estimated time that the spring will last be at [delta]
- * @suppress
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 fun estimateAnimationDurationMillis(
     springConstant: Double,
     dampingCoefficient: Double,
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index 96a15fd..2ae8a21 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -20,7 +20,7 @@
 
 import androidx.annotation.FloatRange
 import androidx.annotation.RestrictTo
-import androidx.collection.MutableObjectList
+import androidx.collection.MutableObjectLongMap
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -29,7 +29,6 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
@@ -44,20 +43,17 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastFold
 import androidx.compose.ui.util.fastForEach
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.resume
+import androidx.compose.ui.util.fastRoundToInt
 import kotlin.math.max
 import kotlin.math.roundToLong
-import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
 
 /**
  * This sets up a [Transition], and updates it with the target provided by [targetState]. When
@@ -198,16 +194,8 @@
 }
 
 /**
- * Lambda to call [SeekableTransitionState.onTotalDurationChanged] when the
- * [Transition.totalDurationNanos] has changed.
- */
-private val SeekableTransitionStateTotalDurationChanged: (SeekableTransitionState<*>) -> Unit = {
-    it.onTotalDurationChanged()
-}
-
-/**
  * A [TransitionState] that can manipulate the progress of the [Transition] by seeking
- * with [seekTo] or animating with [animateTo].
+ * with [snapTo] or animating with [animateTo].
  *
  * A [SeekableTransitionState] can only be used with one [Transition] instance. Once assigned,
  * it cannot be reassigned to a different [Transition] instance.
@@ -222,21 +210,34 @@
         internal set
 
     /**
-     * The value of [targetState] that the composition knows about. Seeking cannot progress until
-     * the composition's target state matches [targetState].
-     */
-    internal var composedTargetState = initialState
-
-    /**
      * The Transition that this is associated with. SeekableTransitionState can only be used
      * with one Transition.
      */
     private var transition: Transition<S>? = null
 
+    /**
+     * The animated fraction for the [currentState] to [targetState].
+     */
+    private var animatedFraction = Animatable(0f).also {
+        it.updateBounds(lowerBound = 0f, upperBound = 1f)
+    }
+
+    /**
+     * The initial value animations from when a seek or animation is interrupted.
+     * The key is the [animatedFraction] at the time of interruption and the value
+     * is the duration of the animation.
+     */
+    private val initialFractionAnimatables =
+        MutableObjectLongMap<Animatable<Float, AnimationVector1D>>()
+
     private val observer = SnapshotStateObserver { it() }
 
     // Used for seekToFraction calculations to avoid allocation
-    internal var totalDurationNanos = 0L
+    private var totalDurationNanos = 0L
+
+    private val reseekToFraction: (Unit) -> Unit = {
+        seekToFraction()
+    }
 
     private val recalculateTotalDurationNanos: () -> Unit = {
         totalDurationNanos = transition?.totalDurationNanos ?: 0L
@@ -249,348 +250,80 @@
      * If [targetState] and [currentState] are the same, [fraction] will be 0.
      */
     @get:FloatRange(from = 0.0, to = 1.0)
-    var fraction: Float by mutableFloatStateOf(0f)
-        private set
-
-    /**
-     * The continuation used when waiting for a composition.
-     */
-    internal var compositionContinuation: CancellableContinuation<S>? = null
-
-    /**
-     * Used to lock the [compositionContinuation] while modifying or checking its value.
-     * Also used for locking the [composedTargetState].
-     */
-    internal val compositionContinuationMutex = Mutex()
-
-    /**
-     * Used to prevent [snapTo], [seekTo], and [animateTo] from running simultaneously.
-     */
-    private val mutatorMutex = MutatorMutex()
-
-    /**
-     * When the animation is running, this contains the most recent frame time. When the
-     * animation has stopped, this is [AnimationConstants.UnspecifiedTime].
-     */
-    private var lastFrameTimeNanos: Long = AnimationConstants.UnspecifiedTime
-
-    /**
-     * List of animation of initial values. The list should be empty when [seekTo],
-     * [snapTo], or [animateTo] completes successfully.
-     */
-    private val initialValueAnimations = MutableObjectList<SeekingAnimationState>()
-
-    /**
-     * When [animateTo] is executing, this is non-null, providing the information being
-     * used to animate its value. This will be null while seeking or after [snapTo].
-     */
-    private var currentAnimation: SeekingAnimationState? = null
-
-    /**
-     * Lambda instance used for capturing the first frame of an animation.
-     */
-    private val firstFrameLambda: (Long) -> Unit = { frameTimeNanos ->
-        lastFrameTimeNanos = frameTimeNanos
-    }
-
-    /**
-     * Used in [animateOneFrameLambda], the duration scale must be set immediately
-     * prior to invoking [withFrameNanos] with [animateOneFrameLambda].
-     */
-    private var durationScale: Float = 0f
-
-    /**
-     * Lambda instance used for animating a single frame within [withFrameNanos].
-     */
-    private val animateOneFrameLambda: (Long) -> Unit = { frameTimeNanos ->
-        val delta = frameTimeNanos - lastFrameTimeNanos
-        lastFrameTimeNanos = frameTimeNanos
-        val deltaPlayTimeNanos = (delta / durationScale).roundToLong()
-        if (initialValueAnimations.isNotEmpty()) {
-            initialValueAnimations.forEach { animation ->
-                // updateInitialValues will set to false if the animation isn't
-                // complete
-                recalculateAnimationValue(animation, deltaPlayTimeNanos)
-                animation.isComplete = true
-            }
-            transition?.updateInitialValues()
-            initialValueAnimations.removeIf { it.isComplete }
-        }
-        val currentAnimation = currentAnimation
-        if (currentAnimation != null) {
-            currentAnimation.durationNanos = totalDurationNanos
-            recalculateAnimationValue(currentAnimation, deltaPlayTimeNanos)
-            fraction = currentAnimation.value
-            if (currentAnimation.value == 1f) {
-                this@SeekableTransitionState.currentAnimation = null // all done!
-            }
-            seekToFraction()
-        }
-    }
-
-    /**
-     * Stops all animations, including the initial value animations and sets the [fraction]
-     * to `1`.
-     */
-    private fun endAllAnimations() {
-        transition?.clearInitialAnimations()
-        initialValueAnimations.clear()
-        val current = currentAnimation
-        if (current != null) {
-            currentAnimation = null
-            fraction = 1f
-            seekToFraction()
-        }
-    }
-
-    /**
-     * Starts the animation. It will advance both the currently-running animation and
-     * initial value animations. If the previous animation was stopped ([seekTo] or [snapTo] or
-     * no previous animation was running), then it will require one frame to capture the
-     * frame time before the animation starts.
-     */
-    private suspend fun runAnimations() {
-        if (initialValueAnimations.isEmpty() && currentAnimation == null) {
-            // nothing to animate
-            return
-        }
-        if (coroutineContext.durationScale == 0f) {
-            endAllAnimations()
-            lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-            return
-        }
-        if (lastFrameTimeNanos == AnimationConstants.UnspecifiedTime) {
-            // have to capture one frame to get the start time
-            withFrameNanos(firstFrameLambda)
-        }
-        while (initialValueAnimations.isNotEmpty() || currentAnimation != null) {
-            animateOneFrame()
-        }
-        lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-    }
-
-    /**
-     * Does one frame of work. If there is no previous animation, then it will capture the
-     * [lastFrameTimeNanos]. If there was a previous animation, it will advance by one frame.
-     */
-    private suspend fun doOneFrame() {
-        if (lastFrameTimeNanos == AnimationConstants.UnspecifiedTime) {
-            // have to capture one frame to get the start time
-            withFrameNanos(firstFrameLambda)
-        } else {
-            animateOneFrame()
-        }
-    }
-
-    /**
-     * Advances all animations by one frame.
-     */
-    private suspend fun animateOneFrame() {
-        val durationScale = coroutineContext.durationScale
-        if (durationScale <= 0f) {
-            endAllAnimations()
-        } else {
-            this@SeekableTransitionState.durationScale = durationScale
-            withFrameNanos(animateOneFrameLambda)
-        }
-    }
-
-    /**
-     * Calculates the [SeekingAnimationState.value] based on the [deltaPlayTimeNanos]. It uses
-     * the animation spec if one is provided, or the progress of the total duration if not. This
-     * method does not account for duration scale.
-     */
-    private fun recalculateAnimationValue(
-        animation: SeekingAnimationState,
-        deltaPlayTimeNanos: Long
-    ) {
-        val playTimeNanos = animation.progressNanos + deltaPlayTimeNanos
-        animation.progressNanos = playTimeNanos
-        val durationNanos = animation.animationSpecDuration
-        if (playTimeNanos >= durationNanos) {
-            animation.value = 1f
-        } else {
-            val animationSpec = animation.animationSpec
-            if (animationSpec != null) {
-                animation.value = animationSpec.getValueFromNanos(
-                    playTimeNanos,
-                    animation.start,
-                    Target1,
-                    animation.initialVelocity ?: ZeroVelocity
-                )[0].coerceIn(0f, 1f)
-            } else {
-                animation.value = lerp(
-                    animation.start[0],
-                    1f,
-                    playTimeNanos.toFloat() / durationNanos
-                )
-            }
-        }
-    }
-
-    /**
-     * Sets [currentState] and [targetState] to `targetState` and snaps all values to those
-     * at that state. The transition will not have any animations running after running
-     * [snapTo].
-     *
-     * This can have a similar effect as [seekTo]. However, [seekTo] moves the [currentState]
-     * to the former [targetState] and animates the initial values of the animations from the
-     * current values to those at [currentState]. [seekTo] also allows the developer to move
-     * the state between any fraction between [currentState] and [targetState], while
-     * [snapTo] moves all state to [targetState] without any further seeking allowed.
-     *
-     * @sample androidx.compose.animation.core.samples.SnapToSample
-     *
-     * @see animateTo
-     */
-    suspend fun snapTo(targetState: S) {
-        val transition = transition ?: return
-        if (currentState == targetState &&
-            this@SeekableTransitionState.targetState == targetState
-        ) {
-            return // nothing to change
-        }
-        mutatorMutex.mutate {
-            endAllAnimations()
-            lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-            fraction = 0f
-            val fraction = when (targetState) {
-                currentState -> ResetAnimationSnapCurrent
-                this@SeekableTransitionState.targetState -> ResetAnimationSnapTarget
-                else -> ResetAnimationSnap
-            }
-            transition.updateTarget(targetState)
-            transition.playTimeNanos = 0L
-            this@SeekableTransitionState.targetState = targetState
-            this@SeekableTransitionState.fraction = 0f
-            currentState = targetState
-            transition.resetAnimationFraction(fraction)
-            if (fraction == ResetAnimationSnap) {
-                // completely changed the value, so we have to wait for a composition to have
-                // the correct animation values
-                waitForCompositionAfterTargetStateChange()
-            }
-        }
-    }
+    val fraction: Float
+        get() = if (currentState == targetState) 0f else animatedFraction.value
 
     /**
      * Starts seeking the transition to [targetState] with [fraction] used to indicate the progress
      * towards [targetState]. If the previous `targetState` was already
-     * [targetState] then [seekTo] only stops any current animation towards that state and snaps
+     * [targetState] then [snapTo] only stops any current animation towards that state and snaps
      * the fraction to the new value. Otherwise, the [currentState] is changed to the former
      * `targetState` and `targetState` is changed to [targetState] and an animation is started,
-     * moving the start values towards the former `targetState`. This will return when the
-     * initial values have reached `currentState` and the [fraction] has been reached.
-     *
-     * [snapTo] also allows the developer to change the state, but does not animate any values.
-     * Instead, it instantly moves all values to those at the new [targetState].
-     *
-     * @sample androidx.compose.animation.core.samples.SeekToSample
+     * moving the start values towards the former `targetState`.
      *
      * @see animateTo
      */
-    suspend fun seekTo(
-        @FloatRange(from = 0.0, to = 1.0) fraction: Float,
-        targetState: S = this.targetState
+    suspend fun snapTo(
+        targetState: S = this.targetState,
+        @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0f
     ) {
         requirePrecondition(fraction in 0f..1f) {
             "Expecting fraction between 0 and 1. Got $fraction"
         }
         val transition = transition ?: return
-        val oldTargetState = this@SeekableTransitionState.targetState
-        mutatorMutex.mutate {
-            coroutineScope {
-                if (targetState != oldTargetState) {
-                    moveAnimationToInitialState()
-                } else {
-                    currentAnimation = null
-                    if (currentState == targetState) {
-                        return@coroutineScope // Can't seek when current state is target state
-                    }
-                }
-                if (targetState != oldTargetState) {
-                    // Change the target _and_ the fraction
-                    transition.updateTarget(targetState)
-                    transition.playTimeNanos = 0L
-                    this@SeekableTransitionState.targetState = targetState
-                    transition.resetAnimationFraction(fraction)
-                }
-                this@SeekableTransitionState.fraction = fraction
-                if (initialValueAnimations.isNotEmpty()) {
-                    launch { runAnimations() }
-                } else {
-                    lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-                }
-                waitForCompositionAfterTargetStateChange()
+        coroutineScope {
+            if (targetState != this@SeekableTransitionState.targetState) {
+                animateInitialState(this@coroutineScope)
+                transition.updateTarget(targetState)
+                this@SeekableTransitionState.currentState = this@SeekableTransitionState.targetState
+                this@SeekableTransitionState.targetState = targetState
+            }
+            if (currentState != targetState) {
+                animatedFraction.snapTo(fraction)
                 seekToFraction()
             }
         }
     }
 
     /**
-     * Wait for composition to set up the target values
-     */
-    private suspend fun waitForCompositionAfterTargetStateChange() {
-        val expectedState = targetState
-        compositionContinuationMutex.lock()
-        if (expectedState == composedTargetState) {
-            compositionContinuationMutex.unlock()
-        } else {
-            val state = suspendCancellableCoroutine { continuation ->
-                compositionContinuation = continuation
-                compositionContinuationMutex.unlock()
-            }
-            if (state != expectedState) {
-                lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-                throw CancellationException(
-                    "snapTo() was canceled because state was changed to " +
-                        "$state instead of $expectedState"
-                )
-            }
-        }
-    }
-
-    /**
-     * Waits for composition, irrespective of whether the target state has changed or not.
-     * This is important for when we're waiting for the currentState to change.
-     */
-    private suspend fun waitForComposition() {
-        val expectedState = targetState
-        compositionContinuationMutex.lock()
-        val state = suspendCancellableCoroutine { continuation ->
-            compositionContinuation = continuation
-            compositionContinuationMutex.unlock()
-        }
-        if (state != expectedState) {
-            lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
-            throw CancellationException("targetState while waiting for composition")
-        }
-    }
-
-    /**
      * Change the animatedInitialFraction to use the animatedFraction, if it needs to be used.
      */
-    private fun moveAnimationToInitialState() {
+    private fun animateInitialState(coroutineScope: CoroutineScope) {
         val transition = transition ?: return
-        val animation = currentAnimation ?: if (totalDurationNanos <= 0 || fraction == 1f ||
-            currentState == targetState
-        ) {
-            null
-        } else {
-            SeekingAnimationState().also {
-                it.value = fraction
-                val totalDurationNanos = totalDurationNanos
-                it.durationNanos = totalDurationNanos
-                it.animationSpecDuration = (totalDurationNanos * (1f - fraction)).roundToLong()
-                it.start[0] = fraction
+        if (animatedFraction.value == 1f || currentState == targetState) {
+            return
+        }
+
+        val currentAnimatable = animatedFraction
+        initialFractionAnimatables[currentAnimatable] = transition.totalDurationNanos
+        animatedFraction = Animatable(0f).also {
+            it.updateBounds(lowerBound = 0f, upperBound = 1f)
+        }
+
+        // Change what it does with the animation
+        transition.setInitialAnimations(currentAnimatable)
+        initialFractionAnimatables.forEach { animatable, duration ->
+            // If the coroutine that launched an animation has been stopped, we must still
+            // continue the animation to its conclusion. This can happen, for example, with
+            // a LaunchedEffect(key) when the key changes.
+            if (!animatable.isRunning) {
+                coroutineScope.launch {
+                    val currentPlayTime = (duration * animatable.value).roundToLong()
+                    val remainderMillis = ((duration - currentPlayTime) / MillisToNanos).toInt()
+                    animatable.animateTo(
+                        1f,
+                        animationSpec = tween(remainderMillis, 0, LinearEasing)
+                    ) {
+                        val initialNanos = (value * duration).roundToLong()
+                        val currentNanos =
+                            (animatedFraction.value * transition.totalDurationNanos).roundToLong()
+                        transition.updateInitialValues(this, initialNanos, currentNanos)
+                    }
+                    transition.clearInitialAnimations(animatable)
+                    initialFractionAnimatables -= animatable
+                }
             }
         }
-        if (animation != null) {
-            animation.durationNanos = totalDurationNanos
-            initialValueAnimations += animation
-            transition.setInitialAnimations(animation)
-        }
-        currentAnimation = null
     }
 
     /**
@@ -612,81 +345,83 @@
         animationSpec: FiniteAnimationSpec<Float>? = null
     ) {
         val transition = transition ?: return
-        mutatorMutex.mutate {
-            coroutineScope {
-                val oldTargetState = this@SeekableTransitionState.targetState
-                if (targetState != oldTargetState) {
-                    moveAnimationToInitialState()
-                    fraction = 0f
-                    transition.updateTarget(targetState)
-                    transition.playTimeNanos = 0L
-                    currentState = oldTargetState
-                    this@SeekableTransitionState.targetState = targetState
-                }
-                val composedTargetState =
-                    compositionContinuationMutex.withLock { composedTargetState }
-                if (targetState != composedTargetState) {
-                    doOneFrame() // We have to wait a frame for the composition, so continue
-                    // Now we shouldn't skip a frame while waiting for composition
-                    waitForCompositionAfterTargetStateChange()
-                }
-                if (currentState != targetState) {
-                    if (fraction < 1f) {
-                        val runningAnimation = currentAnimation
-                        val newSpec = animationSpec?.vectorize(Float.VectorConverter)
-                        if (runningAnimation == null || newSpec != runningAnimation.animationSpec) {
-                            // If there is a running animation, it has changed
-                            val oldSpec = runningAnimation?.animationSpec
-                            val oldVelocity: AnimationVector1D
-                            if (oldSpec != null) {
-                                oldVelocity = oldSpec.getVelocityFromNanos(
-                                    playTimeNanos = runningAnimation.progressNanos,
-                                    initialValue = runningAnimation.start,
-                                    targetValue = Target1,
-                                    initialVelocity =
-                                        runningAnimation.initialVelocity ?: ZeroVelocity
-                                )
-                            } else if (runningAnimation == null ||
-                                runningAnimation.progressNanos == 0L
-                            ) {
-                                oldVelocity = ZeroVelocity
+        var deferred: Deferred<Unit>? = null
+        coroutineScope {
+            if (targetState != this@SeekableTransitionState.targetState) {
+                animateInitialState(this@coroutineScope)
+                animatedFraction.snapTo(0f)
+                transition.updateTarget(targetState)
+                this@SeekableTransitionState.currentState = this@SeekableTransitionState.targetState
+                this@SeekableTransitionState.targetState = targetState
+            }
+            if (animationSpec == null) {
+                val animated = animatedFraction
+                var relaunchAnimateToTargetState: ((Unit) -> Unit)? = null
+                // totalDurationNanos will notify when the value _may have_ changed. We don't
+                // want to change the animation unless the duration really changes.
+                var previousDuration = -1L
+                relaunchAnimateToTargetState = {
+                    if (animated == animatedFraction && animatedFraction.value < 1f) {
+                        observer.observeReads(
+                            Unit,
+                            relaunchAnimateToTargetState!!,
+                            recalculateTotalDurationNanos
+                        )
+                        if (previousDuration != totalDurationNanos) {
+                            previousDuration = totalDurationNanos
+                            val remainderMillis = (totalDurationNanos *
+                                (1f - animated.value) / MillisToNanos).fastRoundToInt()
+
+                            val spec = if (remainderMillis == 0) {
+                                animated.defaultSpringSpec
                             } else {
-                                val oldDurationNanos = runningAnimation.durationNanos
-                                val oldDuration =
-                                    if (oldDurationNanos == AnimationConstants.UnspecifiedTime) {
-                                        totalDurationNanos
-                                    } else {
-                                        oldDurationNanos
-                                    } / (1000f * MillisToNanos)
-                                if (oldDuration <= 0L) {
-                                    oldVelocity = ZeroVelocity
-                                } else {
-                                    oldVelocity = AnimationVector1D(1f / oldDuration)
-                                }
+                                tween(remainderMillis, 0, LinearEasing)
                             }
-                            val newAnimation = runningAnimation ?: SeekingAnimationState()
-                            newAnimation.animationSpec = newSpec
-                            newAnimation.isComplete = false
-                            newAnimation.value = fraction
-                            newAnimation.start[0] = fraction
-                            newAnimation.durationNanos = totalDurationNanos
-                            newAnimation.progressNanos = 0L
-                            newAnimation.initialVelocity = oldVelocity
-                            newAnimation.animationSpecDuration = newSpec?.getDurationNanos(
-                                initialValue = newAnimation.start,
-                                targetValue = Target1,
-                                initialVelocity = oldVelocity
-                            ) ?: (totalDurationNanos * (1f - fraction)).roundToLong()
-                            currentAnimation = newAnimation
+                            deferred = async {
+                                animateToTargetState(spec)
+                            }
                         }
                     }
-                    runAnimations()
-                    currentState = targetState
-                    waitForComposition()
-                    fraction = 0f
                 }
+                relaunchAnimateToTargetState(Unit)
+            } else {
+                animateToTargetState(animationSpec)
             }
         }
+        if (deferred?.isCancelled == true) {
+            throw CancellationException()
+        }
+    }
+
+    /**
+     * Animates from the current fraction to the [targetState] (fraction = 1).
+     *
+     * Upon completion of the animation, [currentState] will be changed to [targetState].
+     */
+    private suspend fun animateToTargetState(animationSpec: FiniteAnimationSpec<Float>) {
+        val transition = transition ?: return
+        if (currentState == targetState) {
+            return
+        }
+
+        val animated = animatedFraction
+        animated.animateTo(targetValue = 1f, animationSpec = animationSpec) {
+            if (this === animatedFraction) {
+                seekToFraction()
+            } else if (this in initialFractionAnimatables) {
+                val durationNanos = initialFractionAnimatables[this]
+                val initialValueNanos = (durationNanos * value).roundToLong()
+                val playTimeNanos =
+                    (animatedFraction.value * transition.totalDurationNanos).roundToLong()
+                transition.updateInitialValues(this, initialValueNanos, playTimeNanos)
+            }
+        }
+        if (animated === animatedFraction) {
+            currentState = targetState
+        } else {
+            transition.clearInitialAnimations(animated)
+            initialFractionAnimatables -= animated
+        }
     }
 
     override fun transitionConfigured(transition: Transition<S>) {
@@ -707,74 +442,17 @@
         }
     }
 
-    internal fun observeTotalDuration() {
-        observer.observeReads(
-            scope = this,
-            onValueChangedForScope = SeekableTransitionStateTotalDurationChanged,
-            block = recalculateTotalDurationNanos
-        )
-    }
-
-    internal fun onTotalDurationChanged() {
-        val previousTotalDurationNanos = totalDurationNanos
-        observeTotalDuration()
-        if (previousTotalDurationNanos != totalDurationNanos) {
-            val animation = currentAnimation
-            if (animation != null) {
-                animation.durationNanos = totalDurationNanos
-                if (animation.animationSpec == null) {
-                    animation.animationSpecDuration =
-                        ((1f - animation.start[0]) * totalDurationNanos).roundToLong()
-                }
-            } else {
-                // seekTo() called with a fraction. If an animation is running, we can just wait
-                // for the animation to change the value. The fraction may not be the best way
-                // to advance a regular animation.
-                seekToFraction()
-            }
-        }
-    }
-
     private fun seekToFraction() {
         val transition = transition ?: return
-        val playTimeNanos = (fraction * transition.totalDurationNanos).roundToLong()
+        observer.observeReads(
+            scope = Unit,
+            onValueChangedForScope = reseekToFraction,
+            block = recalculateTotalDurationNanos
+        )
+        val fraction = animatedFraction.value
+        val playTimeNanos = (fraction * totalDurationNanos).roundToLong()
         transition.seekAnimations(playTimeNanos)
     }
-
-    /**
-     * Contains the state for a running animation. This can be the current animation from
-     * [animateTo] or an initial value animation.
-     */
-    internal class SeekingAnimationState {
-        // The current progress with respect to the animationSpec if it exists or
-        // durationNanos if animationSpec is null
-        var progressNanos: Long = 0L
-        // The AnimationSpec used in this animation, or null if it is a linear animation with
-        // duration of durationNanos
-        var animationSpec: VectorizedAnimationSpec<AnimationVector1D>? = null
-        // Used by initial value animations to mark when the animation should continue
-        var isComplete = false
-        // The current fraction of the animation
-        var value: Float = 0f
-        // The start value of the animation
-        var start: AnimationVector1D = AnimationVector1D(0f)
-        // The initial velocity of the animation
-        var initialVelocity: AnimationVector1D? = null
-        // The total duration of the transition's animations. This is the totalDurationNanos
-        // at the time that this was created for initial value animations. Note that this can
-        // be different from the animationSpec's duration.
-        var durationNanos: Long = 0L
-        // The total duration of the animationSpec. This is kept cached because Spring
-        // animations can take time to calculate their durations
-        var animationSpecDuration: Long = 0L
-    }
-
-    private companion object {
-        // AnimationVector1D with 0 value, kept so that we don't have to allocate unnecessarily
-        val ZeroVelocity = AnimationVector1D(0f)
-        // AnimationVector1D with 1 value, used as the target value of 1f
-        val Target1 = AnimationVector1D(1f)
-    }
 }
 
 /**
@@ -804,16 +482,7 @@
     val transition = remember(transitionState) {
         Transition(transitionState = transitionState, label)
     }
-    if (transitionState is SeekableTransitionState) {
-        LaunchedEffect(transitionState.currentState, transitionState.targetState) {
-            transitionState.observeTotalDuration()
-            transitionState.compositionContinuationMutex.withLock {
-                transitionState.composedTargetState = transitionState.targetState
-                transitionState.compositionContinuation?.resume(transitionState.targetState)
-                transitionState.compositionContinuation = null
-            }
-        }
-    } else {
+    if (transitionState !is SeekableTransitionState) {
         transition.animateTo(transitionState.targetState)
         DisposableEffect(transition) {
             onDispose {
@@ -924,9 +593,9 @@
     /**
      * Play time in nano-seconds. [playTimeNanos] is always non-negative. It starts from 0L at the
      * beginning of the transition and increment until all child animations have finished.
-     * @suppress
      */
-    @InternalAnimationApi
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    @set:RestrictTo(RestrictTo.Scope.LIBRARY)
     var playTimeNanos by mutableLongStateOf(0L)
     private var startTimeNanos by mutableLongStateOf(AnimationConstants.UnspecifiedTime)
 
@@ -949,15 +618,15 @@
         get() = _animations
 
     // Seeking related
-    /** @suppress */
-    @InternalAnimationApi
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    @set:RestrictTo(RestrictTo.Scope.LIBRARY)
     var isSeeking: Boolean by mutableStateOf(false)
         internal set
     internal var lastSeekedTimeNanos = 0L
 
     /**
      * Used internally to know when a [SeekableTransitionState] is animating initial values
-     * after [SeekableTransitionState.animateTo] or [SeekableTransitionState.seekTo] has
+     * after [SeekableTransitionState.animateTo] or [SeekableTransitionState.snapTo] has
      * redirected a transition prior to it completing. This is important for knowing when
      * child transitions must be maintained after a parent target state has changed, but
      * the child target state hasn't changed.
@@ -967,7 +636,7 @@
     @InternalAnimationApi
     @get:InternalAnimationApi
     val hasInitialValueAnimations: Boolean
-        get() = _animations.fastAny { it.initialValueState != null } ||
+        get() = _animations.fastAny { it.initialValueAnimatable != null } ||
             _transitions.fastAny { it.hasInitialValueAnimations }
 
     /**
@@ -996,21 +665,6 @@
         return maxDurationNanos
     }
 
-    /**
-     * Returns `true` when the transition has been reset, likely due to target state change.
-     * After a reset, there's no need to do work for changes in totalDurationNanos because the
-     * animations are invalid.
-     */
-    internal fun isTransitionReset(): Boolean {
-        if (_animations.fastFirstOrNull { it.isReset } != null) {
-            return true
-        }
-        if (_transitions.fastFirstOrNull { it.isTransitionReset() } != null) {
-            return true
-        }
-        return false
-    }
-
     @OptIn(InternalAnimationApi::class)
     internal fun onFrame(frameTimeNanos: Long, durationScale: Float) {
         if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
@@ -1219,49 +873,44 @@
      * current animation is then changed to an unchanging animation that is only moved by
      * the initial value.
      */
-    internal fun setInitialAnimations(
-        animationState: SeekableTransitionState.SeekingAnimationState
+    internal fun setInitialAnimations(animatable: Animatable<Float, AnimationVector1D>) {
+        _animations.fastForEach {
+            it.setInitialValueAnimation(animatable)
+        }
+        _transitions.fastForEach {
+            it.setInitialAnimations(animatable)
+        }
+    }
+
+    /**
+     * Clears the initial value animations that are governed by [animatable].
+     */
+    internal fun clearInitialAnimations(animatable: Animatable<Float, AnimationVector1D>) {
+        _animations.fastForEach {
+            it.clearInitialAnimation(animatable)
+        }
+        _transitions.fastForEach {
+            it.clearInitialAnimations(animatable)
+        }
+    }
+
+    /**
+     * Changes the progress of the initial value governed by [animatable] at the progress
+     * [initialAnimationNanos]. The current animation is governed by [playTimeNanos].
+     */
+    internal fun updateInitialValues(
+        animatable: Animatable<Float, AnimationVector1D>,
+        initialAnimationNanos: Long,
+        playTimeNanos: Long
     ) {
         _animations.fastForEach {
-            it.setInitialValueAnimation(animationState)
+            it.updateInitialValue(animatable, initialAnimationNanos, playTimeNanos)
         }
         _transitions.fastForEach {
-            it.setInitialAnimations(animationState)
+            it.updateInitialValues(animatable, initialAnimationNanos, playTimeNanos)
         }
     }
 
-    /**
-     * Clears all animations. The state has been forced directly to a new value and the
-     * animations are no longer valid.
-     */
-    internal fun resetAnimationFraction(fraction: Float) {
-        _animations.fastForEach { it.resetAnimationValue(fraction) }
-        _transitions.fastForEach { it.resetAnimationFraction(fraction) }
-    }
-
-    /**
-     * Clears all initial value animations.
-     */
-    internal fun clearInitialAnimations() {
-        _animations.fastForEach {
-            it.clearInitialAnimation()
-        }
-        _transitions.fastForEach {
-            it.clearInitialAnimations()
-        }
-    }
-
-    /**
-     * Changes the progress of the initial value.
-     *
-     * @return true if the animationState is animating anything or false if it isn't
-     * animating anything.
-     */
-    internal fun updateInitialValues() {
-        _animations.fastForEach { it.updateInitialValue() }
-        _transitions.fastForEach { it.updateInitialValues() }
-    }
-
     override fun toString(): String {
         return animations.fastFold("Transition animation values: ") { acc, anim -> "$acc$anim, " }
     }
@@ -1316,18 +965,21 @@
         )
             private set
 
-        internal var initialValueState: SeekableTransitionState.SeekingAnimationState? = null
+        internal var initialValueAnimatable: Animatable<Float, AnimationVector1D>?
+            by mutableStateOf(null)
+            private set
         private var initialValueAnimation: TargetBasedAnimation<T, V>? = null
 
         internal var isFinished: Boolean by mutableStateOf(true)
-        internal var resetSnapValue by mutableFloatStateOf(NoReset)
+        private var offsetTimeNanos by mutableLongStateOf(0L)
+        private var needsReset by mutableStateOf(false)
 
         /**
          * When the target state has changed, but the target value remains the same,
          * the initial value animation completely controls the animated value. When
-         * this flag is `true`, the [animation] can be ignored and only the [initialValueState]
+         * this flag is `true`, the [animation] can be ignored and only the [initialValueAnimation]
          * is needed to determine the value. When this is `false`, if there is
-         * an [initialValueState], it is used only for adjusting the initial value
+         * an [initialValueAnimation], it is used only for adjusting the initial value
          * of [animation].
          */
         private var useOnlyInitialValue = false
@@ -1340,16 +992,13 @@
             get() = animation.durationNanos
         private var isSeeking = false
 
-        internal val isReset: Boolean
-            get() = resetSnapValue != NoReset
-
         internal fun onPlayTimeChanged(playTimeNanos: Long, durationScale: Float) {
             val playTime =
                 if (durationScale > 0f) {
-                    val scaledTime = playTimeNanos / durationScale
+                    val scaledTime = (playTimeNanos - offsetTimeNanos) / durationScale
                     checkPrecondition(!scaledTime.isNaN()) {
                         "Duration scale adjusted time is NaN. Duration scale: $durationScale," +
-                            "playTimeNanos: $playTimeNanos"
+                            "playTimeNanos: $playTimeNanos, offsetTimeNanos: $offsetTimeNanos"
                     }
                     scaledTime.toLong()
                 } else {
@@ -1359,11 +1008,12 @@
             velocityVector = animation.getVelocityVectorFromNanos(playTime)
             if (animation.isFinishedFromNanos(playTime)) {
                 isFinished = true
+                offsetTimeNanos = 0
             }
         }
 
         internal fun seekTo(playTimeNanos: Long) {
-            if (resetSnapValue != NoReset) {
+            if (needsReset) {
                 return
             }
             isSeeking = true // SeekableTransitionState won't use interrupted animation spec
@@ -1385,28 +1035,29 @@
          * interrupted, the ongoing animation is moved to changing the initial value. The starting
          * point of the animation is then animated toward the value that would be set at the
          * target state, while the current value is controlled by seeking or animation.
+         *
+         * @param initialValueAnimationPlayTimeNanos The play time of the initial value animation
+         * @param playTimeNanos The play time of the animation of the target
          */
-        internal fun updateInitialValue() {
-            val animState = initialValueState ?: return
-            val animation = initialValueAnimation ?: return
-
-            val initialPlayTimeNanos = (animState.durationNanos * animState.value).roundToLong()
-            val initialValue = animation.getValueFromNanos(initialPlayTimeNanos)
-            if (useOnlyInitialValue) {
-                this.animation.mutableTargetValue = initialValue
+        internal fun updateInitialValue(
+            animatable: Animatable<Float, AnimationVector1D>,
+            initialValueAnimationPlayTimeNanos: Long,
+            playTimeNanos: Long
+        ) {
+            val anim = initialValueAnimation
+            if (initialValueAnimatable !== animatable || anim == null) {
+                return
             }
-            this.animation.mutableInitialValue = initialValue
-            if (resetSnapValue == ResetNoSnap || useOnlyInitialValue) {
+            val initialValue = anim.getValueFromNanos(initialValueAnimationPlayTimeNanos)
+            if (useOnlyInitialValue) {
+                animation.mutableTargetValue = initialValue
+            }
+            animation.mutableInitialValue = initialValue
+            if (needsReset) {
                 value = initialValue
             } else {
                 seekTo(playTimeNanos)
             }
-            if (initialPlayTimeNanos >= animState.durationNanos) {
-                initialValueState = null
-                initialValueAnimation = null
-            } else {
-                animState.isComplete = false
-            }
         }
 
         private val interruptionSpec: FiniteAnimationSpec<T>
@@ -1422,10 +1073,7 @@
             interruptionSpec = spring(visibilityThreshold = visibilityThreshold)
         }
 
-        private fun updateAnimation(
-            initialValue: T = value,
-            isInterrupted: Boolean = false,
-        ) {
+        private fun updateAnimation(initialValue: T = value, isInterrupted: Boolean = false) {
             if (initialValueAnimation?.targetValue == targetValue) {
                 // This animation didn't change the target value, so let the initial value animation
                 // take care of it.
@@ -1439,16 +1087,12 @@
                 useOnlyInitialValue = true
                 return
             }
-            val specWithoutDelay = if (isInterrupted && !isSeeking) {
+            val spec = if (isInterrupted && !isSeeking) {
                 // When interrupted, use the default spring, unless the spec is also a spring.
                 if (animationSpec is SpringSpec<*>) animationSpec else interruptionSpec
             } else {
                 animationSpec
             }
-            val spec = if (playTimeNanos <= 0L) specWithoutDelay else delayed(
-                specWithoutDelay,
-                playTimeNanos
-            )
             animation = TargetBasedAnimation(
                 spec,
                 typeConverter,
@@ -1461,43 +1105,16 @@
         }
 
         internal fun resetAnimation() {
-            resetSnapValue = ResetNoSnap
-        }
-
-        /**
-         * Forces the value to the given fraction or reset value. If [fraction] is
-         * [ResetAnimationSnapCurrent] or [ResetAnimationSnapTarget], the animated values
-         * are directly moved to the start or end of the animation.
-         */
-        internal fun resetAnimationValue(fraction: Float) {
-            if (fraction == ResetAnimationSnapCurrent || fraction == ResetAnimationSnapTarget) {
-                val initAnim = initialValueAnimation
-                if (initAnim != null) {
-                    animation.mutableInitialValue = initAnim.targetValue
-                    initialValueState = null
-                    initialValueAnimation = null
-                }
-
-                val animationValue = if (fraction == ResetAnimationSnapCurrent) {
-                    animation.initialValue
-                } else {
-                    animation.targetValue
-                }
-                animation.mutableInitialValue = animationValue
-                animation.mutableTargetValue = animationValue
-                value = animationValue
-            } else {
-                resetSnapValue = fraction
-            }
+            needsReset = true
         }
 
         internal fun setInitialValueAnimation(
-            animationState: SeekableTransitionState.SeekingAnimationState
+            animatable: Animatable<Float, AnimationVector1D>
         ) {
             if (animation.targetValue != animation.initialValue) {
                 // Continue the animation from the current position to the target
                 initialValueAnimation = animation
-                initialValueState = animationState
+                initialValueAnimatable = animatable
             }
             animation = TargetBasedAnimation(
                 interruptionSpec,
@@ -1509,10 +1126,14 @@
             useOnlyInitialValue = true
         }
 
-        internal fun clearInitialAnimation() {
-            initialValueAnimation = null
-            initialValueState = null
-            useOnlyInitialValue = false
+        internal fun clearInitialAnimation(
+            animatable: Animatable<Float, AnimationVector1D>
+        ) {
+            if (animatable === initialValueAnimatable) {
+                initialValueAnimatable = null
+                initialValueAnimation = null
+                useOnlyInitialValue = false
+            }
         }
 
         override fun toString(): String {
@@ -1522,26 +1143,15 @@
         // This gets called *during* composition
         @OptIn(InternalAnimationApi::class)
         internal fun updateTargetValue(targetValue: T, animationSpec: FiniteAnimationSpec<T>) {
-            if (useOnlyInitialValue && targetValue == initialValueAnimation?.targetValue) {
-                return // we're already animating to the target value through the initial value
+            if (this.targetValue != targetValue || needsReset) {
+                this.targetValue = targetValue
+                this.animationSpec = animationSpec
+                updateAnimation(isInterrupted = !isFinished)
+                isFinished = false
+                // This is needed because the target change could happen during a transition
+                offsetTimeNanos = playTimeNanos
+                needsReset = false
             }
-            if (this.targetValue == targetValue && resetSnapValue == NoReset) {
-                return // nothing to change. Just continue the existing animation.
-            }
-            this.targetValue = targetValue
-            this.animationSpec = animationSpec
-            val initialValue = if (resetSnapValue == ResetAnimationSnap) targetValue else value
-            updateAnimation(initialValue, isInterrupted = !isFinished)
-            isFinished = resetSnapValue == ResetAnimationSnap
-            // This is needed because the target change could happen during a transition
-            if (resetSnapValue >= 0f) {
-                val duration = animation.durationNanos
-                value = animation.getValueFromNanos((duration * resetSnapValue).toLong())
-            } else if (resetSnapValue == ResetAnimationSnap) {
-                value = targetValue
-            }
-            useOnlyInitialValue = false
-            resetSnapValue = NoReset
         }
 
         // This gets called *during* composition
@@ -1610,9 +1220,8 @@
      * Once a [DeferredAnimation] is created, it can be configured and updated as needed using
      * [DeferredAnimation.animate] method.
      *
-     * @suppress
      */
-    @InternalAnimationApi
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     inner class DeferredAnimation<T, V : AnimationVector> internal constructor(
         val typeConverter: TwoWayConverter<T, V>,
         val label: String
@@ -1697,17 +1306,6 @@
     }
 }
 
-// When a TransitionAnimation doesn't need to be reset
-private const val NoReset = -1f
-// When the animation needs to be changed because of a target update
-private const val ResetNoSnap = -2f
-// When the animation should be reset to have the same start and end value
-private const val ResetAnimationSnap = -3f
-// Snap to the current state and set the initial and target values to the same thing
-private const val ResetAnimationSnapCurrent = -4f
-// Snap to the target state and set the initial and target values to the same thing
-private const val ResetAnimationSnapTarget = -5f
-
 /**
  * This creates a [DeferredAnimation], which will not animate until it is set up using
  * [DeferredAnimation.animate]. Once the animation is set up, it will animate from the
@@ -1718,9 +1316,8 @@
  * @param typeConverter A converter to convert any value of type [T] from/to an [AnimationVector]
  * @param label A label for differentiating this animation from others in android studio.
  *
- * @suppress
  */
-@InternalAnimationApi
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 @Composable
 fun <S, T, V : AnimationVector> Transition<S>.createDeferredAnimation(
     typeConverter: TwoWayConverter<T, V>,
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index 1e6d08b..750a320 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -795,6 +795,6 @@
         // After a few frames, we can check the position of the box. If it used the expected
         // transition, its current position should be offset to the right.
         assertEquals(0, boxPosition.y)
-        assertThat(boxPosition.x).isGreaterThan(positionAtInterruption.x)
+        assert(boxPosition.x > positionAtInterruption.x)
     }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
new file mode 100644
index 0000000..0053b63
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input
+
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.invokeGlobalAssertions
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.toOffset
+import androidx.core.view.InputDeviceCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.math.roundToInt
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class BasicTextFieldHandwritingTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val immRule = ComposeInputMethodManagerTestRule()
+
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val Tag = "BasicTextField2"
+
+    private val imm = FakeInputMethodManager()
+
+    @Test
+    fun textField_startStylusHandwriting_unfocused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun textField_startStylusHandwriting_focused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            requestFocus()
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun textField_click_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusInput {
+                down(visibleSize.center.toOffset())
+                move()
+                up()
+            }
+        }
+    }
+
+    @Test
+    fun textField_longClick_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusInput {
+                down(visibleSize.center.toOffset())
+                move(viewConfiguration.longPressTimeoutMillis + 1)
+                up()
+            }
+        }
+    }
+
+    @Test
+    fun textField_longPressAndDrag_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusInput {
+                val startPosition = visibleSize.center.toOffset()
+                down(visibleSize.center.toOffset())
+                val position = startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f)
+                moveTo(
+                    position = position,
+                    delayMillis = viewConfiguration.longPressTimeoutMillis + 1
+                )
+                up()
+            }
+        }
+    }
+
+    @Test
+    fun textField_disabled_notStartStylusHandwriting() {
+        immRule.setFactory { imm }
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                enabled = false
+            )
+        }
+
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+
+        rule.runOnIdle {
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    @Test
+    fun textField_readOnly_notStartStylusHandwriting() {
+        immRule.setFactory { imm }
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                readOnly = true
+            )
+        }
+
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+
+        rule.runOnIdle {
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    private fun testStylusHandwriting(
+        stylusHandwritingStarted: Boolean,
+        interaction: SemanticsNodeInteraction.() -> Unit
+    ) {
+        immRule.setFactory { imm }
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag)
+            )
+        }
+
+        interaction.invoke(rule.onNodeWithTag(Tag))
+
+        rule.runOnIdle {
+            if (stylusHandwritingStarted) {
+                imm.expectCall("startStylusHandwriting")
+            }
+            imm.expectNoMoreCalls()
+        }
+    }
+
+    /** Start stylus handwriting on the target element. */
+    private fun SemanticsNodeInteraction.performStylusHandwriting() {
+        performStylusInput {
+            val startPosition = visibleSize.center.toOffset()
+            down(startPosition)
+            moveTo(startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f))
+            up()
+        }
+    }
+
+    private fun SemanticsNodeInteraction.performStylusInput(
+        block: TouchInjectionScope.() -> Unit
+    ): SemanticsNodeInteraction {
+        @OptIn(ExperimentalTestApi::class)
+        invokeGlobalAssertions()
+        val node = fetchSemanticsNode("Failed to inject stylus input.")
+        val stylusInjectionScope = StylusInjectionScope(node)
+        block.invoke(stylusInjectionScope)
+        return this
+    }
+
+    // We don't have StylusInjectionScope at the moment. This is a simplified implementation for
+    // the basic use cases in this test. It only supports single stylus pointer, and the pointerId
+    // is totally ignored.
+    private inner class StylusInjectionScope(
+        semanticsNode: SemanticsNode
+    ) : TouchInjectionScope, Density by semanticsNode.layoutInfo.density {
+        private val root = semanticsNode.root as ViewRootForTest
+        private val downTime: Long = System.currentTimeMillis()
+
+        private var lastPosition: Offset = Offset.Unspecified
+        private var currentTime: Long = System.currentTimeMillis()
+        private val boundsInRoot = semanticsNode.boundsInRoot
+
+        override val visibleSize: IntSize =
+            IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
+
+        override val viewConfiguration: ViewConfiguration =
+            semanticsNode.layoutInfo.viewConfiguration
+
+        private fun localToRoot(position: Offset): Offset {
+            return position + boundsInRoot.topLeft
+        }
+
+        override fun advanceEventTime(durationMillis: Long) {
+            require(durationMillis >= 0) {
+                "duration of a delay can only be positive, not $durationMillis"
+            }
+            currentTime += durationMillis
+        }
+
+        override fun currentPosition(pointerId: Int): Offset? {
+            return lastPosition
+        }
+
+        override fun down(pointerId: Int, position: Offset) {
+            val rootPosition = localToRoot(position)
+            lastPosition = rootPosition
+            sendTouchEvent(ACTION_DOWN)
+        }
+
+        override fun updatePointerTo(pointerId: Int, position: Offset) {
+            lastPosition = localToRoot(position)
+        }
+
+        override fun move(delayMillis: Long) {
+            advanceEventTime(delayMillis)
+            sendTouchEvent(ACTION_MOVE)
+        }
+
+        @ExperimentalTestApi
+        override fun moveWithHistoryMultiPointer(
+            relativeHistoricalTimes: List<Long>,
+            historicalCoordinates: List<List<Offset>>,
+            delayMillis: Long
+        ) {
+            // Not needed for this test because Android only support one stylus pointer.
+        }
+
+        override fun up(pointerId: Int) {
+            sendTouchEvent(ACTION_UP)
+        }
+
+        override fun cancel(delayMillis: Long) {
+            sendTouchEvent(ACTION_CANCEL)
+        }
+
+        private fun sendTouchEvent(action: Int) {
+            val positionInScreen = run {
+                val array = intArrayOf(0, 0)
+                root.view.getLocationOnScreen(array)
+                Offset(array[0].toFloat(), array[1].toFloat())
+            }
+            val motionEvent = MotionEvent.obtain(
+                /* downTime = */ downTime,
+                /* eventTime = */ currentTime,
+                /* action = */ action,
+                /* pointerCount = */ 1,
+                /* pointerProperties = */ arrayOf(
+                    MotionEvent.PointerProperties().apply {
+                        id = 0
+                        toolType = MotionEvent.TOOL_TYPE_STYLUS
+                    }
+                ),
+                /* pointerCoords = */ arrayOf(
+                    MotionEvent.PointerCoords().apply {
+                        val startOffset = lastPosition
+
+                        // Allows for non-valid numbers/Offsets to be passed along to Compose to
+                        // test if it handles them properly (versus breaking here and we not knowing
+                        // if Compose properly handles these values).
+                        x = if (startOffset.isValid()) {
+                            positionInScreen.x + startOffset.x
+                        } else {
+                            Float.NaN
+                        }
+
+                        y = if (startOffset.isValid()) {
+                            positionInScreen.y + startOffset.y
+                        } else {
+                            Float.NaN
+                        }
+                    }
+                ),
+                /* metaState = */ 0,
+                /* buttonState = */ 0,
+                /* xPrecision = */ 1f,
+                /* yPrecision = */ 1f,
+                /* deviceId = */ 0,
+                /* edgeFlags = */ 0,
+                /* source = */ InputDeviceCompat.SOURCE_TOUCHSCREEN,
+                /* flags = */ 0
+            )
+
+            rule.runOnUiThread {
+                root.view.dispatchTouchEvent(motionEvent)
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
index 0b50dfb..58713f7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
@@ -70,4 +70,8 @@
     override fun sendKeyEvent(event: KeyEvent) {
         calls += "sendKeyEvent"
     }
+
+    override fun startStylusHandwriting() {
+        calls += "startStylusHandwriting"
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
index fdb0902..a093e88 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
@@ -87,6 +87,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.ceil
 import kotlin.math.floor
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -532,6 +533,7 @@
             .assertCursor(cursorTopCenterInLtr)
     }
 
+    @Ignore // b/327235206
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun togglingInnerTextField_whileFocused_cursorContinuesToDraw() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
index f357dfc..c787d07 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
@@ -93,6 +93,7 @@
         }
 
         override fun sendKeyEvent(event: KeyEvent) {}
+        override fun startStylusHandwriting() {}
     }
 
     private lateinit var inputConnection: InputConnection
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
index 554b743..bd5991a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.text.input.KeyboardType
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.launch
 
 /** Enable to print logs during debugging, see [logDebug]. */
@@ -43,7 +44,8 @@
     layoutState: TextLayoutState,
     imeOptions: ImeOptions,
     receiveContentConfiguration: ReceiveContentConfiguration?,
-    onImeAction: ((ImeAction) -> Unit)?
+    onImeAction: ((ImeAction) -> Unit)?,
+    stylusHandwritingTrigger: MutableSharedFlow<Unit>?
 ): Nothing {
     platformSpecificTextInputSession(
         state = state,
@@ -51,7 +53,8 @@
         imeOptions = imeOptions,
         receiveContentConfiguration = receiveContentConfiguration,
         onImeAction = onImeAction,
-        composeImm = ComposeInputMethodManager(view)
+        composeImm = ComposeInputMethodManager(view),
+        stylusHandwritingTrigger = stylusHandwritingTrigger,
     )
 }
 
@@ -62,7 +65,8 @@
     imeOptions: ImeOptions,
     receiveContentConfiguration: ReceiveContentConfiguration?,
     onImeAction: ((ImeAction) -> Unit)?,
-    composeImm: ComposeInputMethodManager
+    composeImm: ComposeInputMethodManager,
+    stylusHandwritingTrigger: MutableSharedFlow<Unit>?
 ): Nothing {
     coroutineScope {
         launch(start = CoroutineStart.UNDISPATCHED) {
@@ -92,6 +96,12 @@
             }
         }
 
+        stylusHandwritingTrigger?.let {
+            launch(start = CoroutineStart.UNDISPATCHED) {
+                it.collect { composeImm.startStylusHandwriting() }
+            }
+        }
+
         val cursorUpdatesController = CursorAnchorInfoController(
             composeImm = composeImm,
             textFieldState = state,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ComposeInputMethodManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ComposeInputMethodManager.android.kt
index 643c828..4e4eee14 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ComposeInputMethodManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ComposeInputMethodManager.android.kt
@@ -64,6 +64,11 @@
      * delegation when the InputConnection itself does not handle the received event.
      */
     fun sendKeyEvent(event: KeyEvent)
+
+    /**
+     * Signal the IME to start stylus handwriting.
+     */
+    fun startStylusHandwriting()
 }
 
 /**
@@ -78,6 +83,7 @@
 /** This lets us swap out the implementation in our own tests. */
 private var ComposeInputMethodManagerFactory: (View) -> ComposeInputMethodManager = { view ->
     when {
+        Build.VERSION.SDK_INT >= 34 -> ComposeInputMethodManagerImplApi34(view)
         Build.VERSION.SDK_INT >= 24 -> ComposeInputMethodManagerImplApi24(view)
         else -> ComposeInputMethodManagerImplApi21(view)
     }
@@ -146,6 +152,10 @@
         requireImm().updateCursorAnchorInfo(view, info)
     }
 
+    override fun startStylusHandwriting() {
+        // stylus handwriting is only supported after Android U.
+    }
+
     protected fun requireImm(): InputMethodManager = imm ?: createImm().also { imm = it }
 
     private fun createImm() =
@@ -176,3 +186,11 @@
         requireImm().dispatchKeyEventFromInputMethod(view, event)
     }
 }
+
+@RequiresApi(34)
+private open class ComposeInputMethodManagerImplApi34(view: View) :
+    ComposeInputMethodManagerImplApi24(view) {
+    override fun startStylusHandwriting() {
+        requireImm().startStylusHandwriting(view)
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 08f3e2c..1d76f11 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -748,6 +748,33 @@
         y = if (orientation == Orientation.Vertical) this else 0f,
     )
 
+    private var latestScrollScope: ScrollScope = NoOpScrollScope
+    private var latestScrollSource: NestedScrollSource = NestedScrollSource.Drag
+
+    private val performScroll: (delta: Offset) -> Offset = { delta ->
+        val consumedByPreScroll =
+            nestedScrollDispatcher.dispatchPreScroll(delta, latestScrollSource)
+
+        val scrollAvailableAfterPreScroll = delta - consumedByPreScroll
+
+        val singleAxisDeltaForSelfScroll =
+            scrollAvailableAfterPreScroll.singleAxisOffset().reverseIfNeeded().toFloat()
+
+        // Consume on a single axis.
+        val consumedBySelfScroll =
+            with(latestScrollScope) {
+                scrollBy(singleAxisDeltaForSelfScroll).toOffset().reverseIfNeeded()
+            }
+
+        val deltaAvailableAfterScroll = scrollAvailableAfterPreScroll - consumedBySelfScroll
+        val consumedByPostScroll = nestedScrollDispatcher.dispatchPostScroll(
+            consumedBySelfScroll,
+            deltaAvailableAfterScroll,
+            latestScrollSource
+        )
+        consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll
+    }
+
     /**
      * @return the amount of scroll that was consumed
      */
@@ -755,29 +782,9 @@
         initialAvailableDelta: Offset,
         source: NestedScrollSource
     ): Offset {
-        val performScroll: (Offset) -> Offset = { delta ->
-            val consumedByPreScroll = nestedScrollDispatcher.dispatchPreScroll(delta, source)
-
-            val scrollAvailableAfterPreScroll = delta - consumedByPreScroll
-
-            val singleAxisDeltaForSelfScroll =
-                scrollAvailableAfterPreScroll.singleAxisOffset().reverseIfNeeded().toFloat()
-
-            // Consume on a single axis
-            val consumedBySelfScroll =
-                scrollBy(singleAxisDeltaForSelfScroll).toOffset().reverseIfNeeded()
-
-            val deltaAvailableAfterScroll = scrollAvailableAfterPreScroll - consumedBySelfScroll
-            val consumedByPostScroll = nestedScrollDispatcher.dispatchPostScroll(
-                consumedBySelfScroll,
-                deltaAvailableAfterScroll,
-                source
-            )
-            consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll
-        }
-
+        latestScrollSource = source
+        latestScrollScope = this
         val overscroll = overscrollEffect
-
         return if (source == Wheel) {
             performScroll(initialAvailableDelta)
         } else if (overscroll != null && shouldDispatchOverscroll) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index ee317a6..bb39f14 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -23,6 +23,8 @@
 import androidx.compose.foundation.content.internal.dragAndDropRequestPermission
 import androidx.compose.foundation.content.internal.getReceiveContentConfiguration
 import androidx.compose.foundation.content.readPlainText
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.text.BasicTextField
@@ -43,6 +45,9 @@
 import androidx.compose.ui.input.key.KeyInputModifierNode
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
@@ -60,6 +65,7 @@
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.PlatformTextInputModifierNode
 import androidx.compose.ui.platform.PlatformTextInputSession
@@ -88,8 +94,13 @@
 import androidx.compose.ui.text.input.KeyboardCapitalization
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastFirstOrNull
 import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalFoundationApi::class)
@@ -178,25 +189,117 @@
     LayoutAwareModifierNode {
 
     private val editable get() = enabled && !readOnly
+    private var _stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+    private val stylusHandwritingTrigger: MutableSharedFlow<Unit>
+        get() {
+            val handwritingTrigger = _stylusHandwritingTrigger
+            if (handwritingTrigger != null) return handwritingTrigger
+            return MutableSharedFlow<Unit>(
+                replay = 1,
+                onBufferOverflow = BufferOverflow.DROP_LATEST
+            ).also { _stylusHandwritingTrigger = it }
+        }
 
     private val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
-        with(textFieldSelectionState) {
-            textFieldGestures(
-                requestFocus = {
+        coroutineScope {
+            with(textFieldSelectionState) {
+                val requestFocus = {
                     if (!isFocused) requestFocus()
-                },
-                showKeyboard = {
-                    if (inputSessionJob != null) {
-                        // just reshow the keyboard in existing session
-                        requireKeyboardController().show()
-                    } else {
-                        startInputSession(fromTap = true)
-                    }
                 }
-            )
+
+                launch(start = CoroutineStart.UNDISPATCHED) {
+                    detectTouchMode()
+                }
+                launch(start = CoroutineStart.UNDISPATCHED) {
+                    detectTextFieldTapGestures(
+                        requestFocus = requestFocus,
+                        showKeyboard = {
+                            if (inputSessionJob != null) {
+                                // just reshow the keyboard in existing session
+                                requireKeyboardController().show()
+                            } else {
+                                startInputSession(fromTap = true)
+                            }
+                        })
+                }
+                launch(start = CoroutineStart.UNDISPATCHED) {
+                    detectTextFieldLongPressAndAfterDrag(requestFocus)
+                }
+            }
+            launch(start = CoroutineStart.UNDISPATCHED) {
+                detectStylusHandwriting()
+            }
         }
     })
 
+    private suspend fun PointerInputScope.detectStylusHandwriting() {
+        awaitEachGesture {
+            val firstDown =
+                awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+            val isStylus =
+                firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+            if (!editable || !isStylus) {
+                return@awaitEachGesture
+            }
+
+            val viewConfiguration = currentValueOf(LocalViewConfiguration)
+
+            // Await the touch slop before long press timeout.
+            var exceedsTouchSlop: PointerInputChange? = null
+            // The stylus move must exceeds touch slop before long press timeout.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
+                // The tracked pointer is consumed or lifted, stop tracking.
+                val change = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                }
+                if (change == null) {
+                    break
+                }
+
+                val time = change.uptimeMillis - firstDown.uptimeMillis
+                if (time >= viewConfiguration.longPressTimeoutMillis) {
+                    break
+                }
+
+                val offset = change.position - firstDown.position
+                if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+                    exceedsTouchSlop = change
+                    break
+                }
+            }
+
+            if (exceedsTouchSlop == null) return@awaitEachGesture
+
+            exceedsTouchSlop.consume()
+
+            if (!isFocused) {
+                requestFocus()
+            }
+
+            // Send the handwriting start signal to platform.
+            // The editor should send the signal when it is focused or is about to gain focused,
+            // Here are more details:
+            //   1) if the editor already has an active input session, the platform handwriting
+            //   service should already listen to this flow and it'll start handwriting right away.
+            //
+            //   2) if the editor is not focused, but it'll be focused and create a new input
+            //   session, one handwriting signal will be replayed when the platform collect this
+            //   flow. And the platform should trigger handwriting accordingly.
+            stylusHandwritingTrigger.tryEmit(Unit)
+
+            // Consume the remaining changes of this pointer.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+                val pointerChange = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                } ?: return@awaitEachGesture
+                pointerChange.consume()
+            }
+        }
+    }
+
     /**
      * The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
      * need to keep a reference to this event to send a follow-up exit event.
@@ -642,15 +745,18 @@
                     layoutState = textLayoutState,
                     imeOptions = keyboardOptions.toImeOptions(singleLine),
                     receiveContentConfiguration = receiveContentConfiguration,
-                    onImeAction = onImeActionPerformed
+                    onImeAction = onImeActionPerformed,
+                    stylusHandwritingTrigger = stylusHandwritingTrigger,
                 )
             }
         }
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     private fun disposeInputSession() {
         inputSessionJob?.cancel()
         inputSessionJob = null
+        stylusHandwritingTrigger.resetReplayCache()
     }
 
     private fun startInputSessionOnWindowFocusChange() {
@@ -682,7 +788,8 @@
     layoutState: TextLayoutState,
     imeOptions: ImeOptions,
     receiveContentConfiguration: ReceiveContentConfiguration?,
-    onImeAction: ((ImeAction) -> Unit)?
+    onImeAction: ((ImeAction) -> Unit)?,
+    stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
 ): Nothing
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 34f2cc4..f3c22d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -344,27 +344,6 @@
             }
         }
     }
-
-    /**
-     * Implements the complete set of gestures supported by the TextField area.
-     */
-    suspend fun PointerInputScope.textFieldGestures(
-        requestFocus: () -> Unit,
-        showKeyboard: () -> Unit
-    ) {
-        coroutineScope {
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                detectTouchMode()
-            }
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                detectTextFieldTapGestures(requestFocus, showKeyboard)
-            }
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                detectTextFieldLongPressAndAfterDrag(requestFocus)
-            }
-        }
-    }
-
     /**
      * Gesture detector for dragging the selection handles to change the selection in TextField.
      */
@@ -432,7 +411,7 @@
      * This helper gesture detector should be added to all TextField pointer input receivers such
      * as TextFieldDecorator, cursor handle, and selection handles.
      */
-    private suspend fun PointerInputScope.detectTouchMode() {
+    suspend fun PointerInputScope.detectTouchMode() {
         awaitPointerEventScope {
             while (true) {
                 val event = awaitPointerEvent(PointerEventPass.Initial)
@@ -441,7 +420,7 @@
         }
     }
 
-    private suspend fun PointerInputScope.detectTextFieldTapGestures(
+    suspend fun PointerInputScope.detectTextFieldTapGestures(
         requestFocus: () -> Unit,
         showKeyboard: () -> Unit
     ) {
@@ -614,7 +593,7 @@
         }
     }
 
-    private suspend fun PointerInputScope.detectTextFieldLongPressAndAfterDrag(
+    suspend fun PointerInputScope.detectTextFieldLongPressAndAfterDrag(
         requestFocus: () -> Unit
     ) {
         var dragBeginOffsetInText = -1
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt
index 1c55c8a..d49066a 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableSharedFlow
 
 /**
  * Runs desktop-specific text input session logic.
@@ -30,7 +31,8 @@
     layoutState: TextLayoutState,
     imeOptions: ImeOptions,
     receiveContentConfiguration: ReceiveContentConfiguration?,
-    onImeAction: ((ImeAction) -> Unit)?
+    onImeAction: ((ImeAction) -> Unit)?,
+    stylusHandwritingTrigger: MutableSharedFlow<Unit>?
 ): Nothing {
     // TODO(b/267235947) Wire up desktop.
     awaitCancellation()
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index a0b60c9..483a851 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -42,6 +42,7 @@
 import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
 import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.DrawModifierNode
 import androidx.compose.ui.node.invalidateDraw
 import androidx.compose.ui.unit.Dp
@@ -65,6 +66,11 @@
  * factories directly: this node exists for design system libraries to delegate their Ripple
  * implementation to, after querying any required theme values for customizing the Ripple.
  *
+ * NOTE: when using this factory with [DelegatingNode.delegate], ensure that the node is created
+ * once or [DelegatingNode.undelegate] is called in [Modifier.Node.onDetach]. Repeatedly delegating
+ * to a new node returned by this method in [Modifier.Node.onAttach] without removing the old one
+ * will result in multiple ripple nodes being attached to the node.
+ *
  * @param interactionSource the [InteractionSource] used to determine the state of the ripple.
  * @param bounded if true, ripples are clipped by the bounds of the target layout. Unbounded
  * ripples always animate from the target layout center, bounded ripples animate from the touch
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
index 51f89e5..d39835c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
@@ -42,7 +42,8 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
         assertThat(motions).isEqualTo(ThreePaneMotion.NoMotion)
     }
@@ -60,9 +61,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToLeftMotion)
+        assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
     }
 
     @Test
@@ -78,9 +80,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToLeftMotion)
+        assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
     }
 
     @Test
@@ -96,9 +99,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToLeftMotion)
+        assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
     }
 
     @Test
@@ -114,9 +118,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToRightMotion)
+        assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
     }
 
     @Test
@@ -132,9 +137,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToRightMotion)
+        assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
     }
 
     @Test
@@ -150,9 +156,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToRightMotion)
+        assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
     }
 
     @Test
@@ -168,9 +175,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToLeftMotion)
+        assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
     }
 
     @Test
@@ -186,9 +194,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.movePanesToRightMotion)
+        assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
     }
 
     @Test
@@ -204,9 +213,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.switchRightTwoPanesMotion)
+        assertThat(motions).isEqualTo(SwitchRightTwoPanesMotion(SpacerSize))
     }
 
     @Test
@@ -222,9 +232,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.switchRightTwoPanesMotion)
+        assertThat(motions).isEqualTo(SwitchRightTwoPanesMotion(SpacerSize))
     }
 
     @Test
@@ -240,9 +251,10 @@
                 PaneAdaptedValue.Expanded,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.switchLeftTwoPanesMotion)
+        assertThat(motions).isEqualTo(SwitchLeftTwoPanesMotion(SpacerSize))
     }
 
     @Test
@@ -258,9 +270,10 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Expanded
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
-        assertThat(motions).isEqualTo(ThreePaneMotionDefaults.switchLeftTwoPanesMotion)
+        assertThat(motions).isEqualTo(SwitchLeftTwoPanesMotion(SpacerSize))
     }
 
     @Test
@@ -277,7 +290,8 @@
                 PaneAdaptedValue.Hidden,
                 PaneAdaptedValue.Hidden
             ),
-            PaneOrder
+            PaneOrder,
+            SpacerSize
         )
         assertThat(motions).isEqualTo(ThreePaneMotion.NoMotion)
     }
@@ -340,3 +354,4 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal val PaneOrder = ThreePaneScaffoldDefaults.SupportingPaneLayoutPaneOrder
+internal const val SpacerSize = 123
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
index 3395ad3..6e671ff 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
@@ -38,7 +38,7 @@
  */
 @ExperimentalMaterial3AdaptiveApi
 @Immutable
-internal class ThreePaneMotion internal constructor(
+internal open class ThreePaneMotion internal constructor(
     internal val animationSpec: FiniteAnimationSpec<IntOffset> = snap(),
     private val firstPaneEnterTransition: EnterTransition = EnterTransition.None,
     private val firstPaneExitTransition: ExitTransition = ExitTransition.None,
@@ -114,6 +114,130 @@
          * [ExitTransition.None].
          */
         val NoMotion = ThreePaneMotion()
+
+        @JvmStatic
+        protected fun slideInFromLeft(spacerSize: Int) =
+            slideInHorizontally(ThreePaneMotionDefaults.PaneSpringSpec) { -it - spacerSize }
+
+        @JvmStatic
+        protected fun slideInFromLeftDelayed(spacerSize: Int) =
+            slideInHorizontally(ThreePaneMotionDefaults.PaneSpringSpecDelayed) { -it - spacerSize }
+
+        @JvmStatic
+        protected fun slideInFromRight(spacerSize: Int) =
+            slideInHorizontally(ThreePaneMotionDefaults.PaneSpringSpec) { it + spacerSize }
+
+        @JvmStatic
+        protected fun slideInFromRightDelayed(spacerSize: Int) =
+            slideInHorizontally(ThreePaneMotionDefaults.PaneSpringSpecDelayed) { it + spacerSize }
+
+        @JvmStatic
+        protected fun slideOutToLeft(spacerSize: Int) =
+            slideOutHorizontally(ThreePaneMotionDefaults.PaneSpringSpec) { -it - spacerSize }
+
+        @JvmStatic
+        protected fun slideOutToRight(spacerSize: Int) =
+            slideOutHorizontally(ThreePaneMotionDefaults.PaneSpringSpec) { it + spacerSize }
+    }
+}
+
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class MovePanesToLeftMotion(
+    private val spacerSize: Int
+) : ThreePaneMotion(
+    ThreePaneMotionDefaults.PaneSpringSpec,
+    slideInFromRight(spacerSize),
+    slideOutToLeft(spacerSize),
+    slideInFromRight(spacerSize),
+    slideOutToLeft(spacerSize),
+    slideInFromRight(spacerSize),
+    slideOutToLeft(spacerSize)
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is MovePanesToLeftMotion) return false
+        if (this.spacerSize != other.spacerSize) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return spacerSize
+    }
+}
+
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class MovePanesToRightMotion(
+    private val spacerSize: Int
+) : ThreePaneMotion(
+    ThreePaneMotionDefaults.PaneSpringSpec,
+    slideInFromLeft(spacerSize),
+    slideOutToRight(spacerSize),
+    slideInFromLeft(spacerSize),
+    slideOutToRight(spacerSize),
+    slideInFromLeft(spacerSize),
+    slideOutToRight(spacerSize)
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is MovePanesToRightMotion) return false
+        if (this.spacerSize != other.spacerSize) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return spacerSize
+    }
+}
+
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class SwitchLeftTwoPanesMotion(
+    private val spacerSize: Int
+) : ThreePaneMotion(
+    ThreePaneMotionDefaults.PaneSpringSpec,
+    slideInFromLeftDelayed(spacerSize),
+    slideOutToLeft(spacerSize),
+    slideInFromLeftDelayed(spacerSize),
+    slideOutToLeft(spacerSize),
+    EnterTransition.None,
+    ExitTransition.None
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SwitchLeftTwoPanesMotion) return false
+        if (this.spacerSize != other.spacerSize) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return spacerSize
+    }
+}
+
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class SwitchRightTwoPanesMotion(
+    private val spacerSize: Int
+) : ThreePaneMotion(
+    ThreePaneMotionDefaults.PaneSpringSpec,
+    EnterTransition.None,
+    ExitTransition.None,
+    slideInFromRightDelayed(spacerSize),
+    slideOutToRight(spacerSize),
+    slideInFromRightDelayed(spacerSize),
+    slideOutToRight(spacerSize)
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SwitchRightTwoPanesMotion) return false
+        if (this.spacerSize != other.spacerSize) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return spacerSize
     }
 }
 
@@ -121,7 +245,8 @@
 internal fun calculateThreePaneMotion(
     previousScaffoldValue: ThreePaneScaffoldValue,
     currentScaffoldValue: ThreePaneScaffoldValue,
-    paneOrder: ThreePaneScaffoldHorizontalOrder
+    paneOrder: ThreePaneScaffoldHorizontalOrder,
+    spacerSize: Int
 ): ThreePaneMotion {
     if (previousScaffoldValue.equals(currentScaffoldValue)) {
         return ThreePaneMotion.NoMotion
@@ -135,19 +260,19 @@
     return when (previousExpandedCount) {
         1 -> when (PaneAdaptedValue.Expanded) {
             previousScaffoldValue[paneOrder.firstPane] -> {
-                ThreePaneMotionDefaults.movePanesToLeftMotion
+                MovePanesToLeftMotion(spacerSize)
             }
 
             previousScaffoldValue[paneOrder.thirdPane] -> {
-                ThreePaneMotionDefaults.movePanesToRightMotion
+                MovePanesToRightMotion(spacerSize)
             }
 
             currentScaffoldValue[paneOrder.thirdPane] -> {
-                ThreePaneMotionDefaults.movePanesToLeftMotion
+                MovePanesToLeftMotion(spacerSize)
             }
 
             else -> {
-                ThreePaneMotionDefaults.movePanesToRightMotion
+                MovePanesToRightMotion(spacerSize)
             }
         }
 
@@ -155,24 +280,24 @@
             previousScaffoldValue[paneOrder.firstPane] == PaneAdaptedValue.Expanded &&
                 currentScaffoldValue[paneOrder.firstPane] == PaneAdaptedValue.Expanded -> {
                 // The first pane stays, the right two panes switch
-                ThreePaneMotionDefaults.switchRightTwoPanesMotion
+                SwitchRightTwoPanesMotion(spacerSize)
             }
 
             previousScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded &&
                 currentScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded -> {
                 // The third pane stays, the left two panes switch
-                ThreePaneMotionDefaults.switchLeftTwoPanesMotion
+                SwitchLeftTwoPanesMotion(spacerSize)
             }
 
             // Implies the second pane stays hereafter
             currentScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded -> {
                 // The third pane shows, all panes move left
-                ThreePaneMotionDefaults.movePanesToLeftMotion
+                MovePanesToLeftMotion(spacerSize)
             }
 
             else -> {
                 // The first pane shows, all panes move right
-                ThreePaneMotionDefaults.movePanesToRightMotion
+                MovePanesToRightMotion(spacerSize)
             }
         }
 
@@ -287,51 +412,4 @@
             delayedRatio = 0.1f,
             visibilityThreshold = IntOffset.VisibilityThreshold
         )
-
-    private val slideInFromLeft = slideInHorizontally(PaneSpringSpec) { -it }
-    private val slideInFromLeftDelayed = slideInHorizontally(PaneSpringSpecDelayed) { -it }
-    private val slideInFromRight = slideInHorizontally(PaneSpringSpec) { it }
-    private val slideInFromRightDelayed = slideInHorizontally(PaneSpringSpecDelayed) { it }
-    private val slideOutToLeft = slideOutHorizontally(PaneSpringSpec) { -it }
-    private val slideOutToRight = slideOutHorizontally(PaneSpringSpec) { it }
-
-    val movePanesToRightMotion = ThreePaneMotion(
-        PaneSpringSpec,
-        slideInFromLeft,
-        slideOutToRight,
-        slideInFromLeft,
-        slideOutToRight,
-        slideInFromLeft,
-        slideOutToRight
-    )
-
-    val movePanesToLeftMotion = ThreePaneMotion(
-        PaneSpringSpec,
-        slideInFromRight,
-        slideOutToLeft,
-        slideInFromRight,
-        slideOutToLeft,
-        slideInFromRight,
-        slideOutToLeft
-    )
-
-    val switchLeftTwoPanesMotion = ThreePaneMotion(
-        PaneSpringSpec,
-        slideInFromLeftDelayed,
-        slideOutToLeft,
-        slideInFromLeftDelayed,
-        slideOutToLeft,
-        EnterTransition.None,
-        ExitTransition.None
-    )
-
-    val switchRightTwoPanesMotion = ThreePaneMotion(
-        PaneSpringSpec,
-        EnterTransition.None,
-        ExitTransition.None,
-        slideInFromRightDelayed,
-        slideOutToRight,
-        slideInFromRightDelayed,
-        slideOutToRight
-    )
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index 0471d58..28633ca 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.layout.MultiContentMeasurePolicy
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
@@ -92,7 +93,10 @@
     val paneMotion = calculateThreePaneMotion(
         previousScaffoldValue = previousScaffoldValue.value,
         currentScaffoldValue = scaffoldValue,
-        paneOrder = ltrPaneOrder
+        paneOrder = ltrPaneOrder,
+        spacerSize = with(LocalDensity.current) {
+            scaffoldDirective.horizontalPartitionSpacerSize.roundToPx()
+        }
     )
     previousScaffoldValue.value = scaffoldValue
 
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 669eeda..8788917 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1485,10 +1485,10 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional float thumbTrackGapSize, optional float trackInsideCornerSize, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator);
+    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator, optional float thumbTrackGapSize, optional float trackInsideCornerSize);
     method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional float thumbTrackGapSize, optional float trackInsideCornerSize, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator, optional float thumbTrackGapSize, optional float trackInsideCornerSize);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 669eeda..8788917 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1485,10 +1485,10 @@
   @androidx.compose.runtime.Stable public final class SliderDefaults {
     method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
     method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional float thumbTrackGapSize, optional float trackInsideCornerSize, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator);
+    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.RangeSliderState rangeSliderState, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator, optional float thumbTrackGapSize, optional float trackInsideCornerSize);
     method @Deprecated @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional float thumbTrackGapSize, optional float trackInsideCornerSize, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderState sliderState, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.graphics.drawscope.DrawScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? drawStopIndicator, optional float thumbTrackGapSize, optional float trackInsideCornerSize);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Themes.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Themes.kt
index 452be64..dd924b5 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Themes.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Themes.kt
@@ -79,12 +79,16 @@
      *
      * If the dynamic colors are not available, the baseline color scheme will be used as a fallback.
      */
-    Dynamic("Dynamic (Android 12+)"),
+    Dynamic("Dynamic (Android 12+)");
+
+    override fun toString(): String = label
 }
 
 enum class FontScaleMode(val label: String) {
     Custom("Custom"),
-    System("System"),
+    System("System");
+
+    override fun toString(): String = label
 }
 
 enum class ThemeMode {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
index 668c591..6156e44 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
@@ -36,13 +36,11 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
-import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -63,7 +61,6 @@
     onFavoriteClick: () -> Unit,
     content: @Composable (PaddingValues) -> Unit
 ) {
-    val coroutineScope = rememberCoroutineScope()
     val context = LocalContext.current
     val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
     val sheetState = rememberModalBottomSheetState()
@@ -100,16 +97,7 @@
             content = {
                 ThemePicker(
                     theme = theme,
-                    onThemeChange = { theme ->
-                        coroutineScope.launch {
-                            sheetState.hide()
-                            onThemeChange(theme)
-                        }.invokeOnCompletion {
-                            if (!sheetState.isVisible) {
-                                openThemePicker = false
-                            }
-                        }
-                    }
+                    onThemeChange = onThemeChange,
                 )
             },
         )
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogTopAppBar.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogTopAppBar.kt
index 20cec86..aa7c923 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogTopAppBar.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogTopAppBar.kt
@@ -82,19 +82,19 @@
                                 MaterialTheme.colorScheme.primary
                             else
                                 LocalContentColor.current,
-                            contentDescription = null
+                            contentDescription = stringResource(id = R.string.favorite_button)
                         )
                     }
                     IconButton(onClick = onThemeClick) {
                         Icon(
                             painter = painterResource(id = R.drawable.ic_palette_24dp),
-                            contentDescription = null
+                            contentDescription = stringResource(id = R.string.change_theme_button)
                         )
                     }
                     IconButton(onClick = { moreMenuExpanded = true }) {
                         Icon(
                             imageVector = Icons.Default.MoreVert,
-                            contentDescription = null
+                            contentDescription = stringResource(id = R.string.more_menu_button)
                         )
                     }
                 }
@@ -137,7 +137,7 @@
                 IconButton(onClick = onBackClick) {
                     Icon(
                         imageVector = Icons.AutoMirrored.Default.ArrowBack,
-                        contentDescription = null
+                        contentDescription = stringResource(id = R.string.back_button)
                     )
                 }
             }
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/Theme.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/Theme.kt
index d129b4c..a7f35ae 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/Theme.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/Theme.kt
@@ -16,10 +16,10 @@
 
 package androidx.compose.material3.catalog.library.ui.theme
 
+import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.Context
 import android.content.ContextWrapper
-import android.os.Build
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material3.ColorScheme
 import androidx.compose.material3.MaterialTheme
@@ -44,33 +44,28 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.core.view.WindowCompat
 
+@SuppressLint("NewApi")
 @Composable
 fun CatalogTheme(
     theme: Theme,
     content: @Composable () -> Unit
 ) {
-    val colorScheme =
-        if (theme.colorMode == ColorMode.Dynamic &&
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            val context = LocalContext.current
-            colorSchemeFromThemeMode(
-                themeMode = theme.themeMode,
-                lightColorScheme = dynamicLightColorScheme(context),
-                darkColorScheme = dynamicDarkColorScheme(context),
-            )
-        } else if (theme.colorMode == ColorMode.Custom) {
-            colorSchemeFromThemeMode(
-                themeMode = theme.themeMode,
-                lightColorScheme = LightCustomColorScheme,
-                darkColorScheme = DarkCustomColorScheme,
-            )
-        } else {
-            colorSchemeFromThemeMode(
-                themeMode = theme.themeMode,
-                lightColorScheme = lightColorScheme(),
-                darkColorScheme = darkColorScheme(),
-            )
-        }
+    val context = LocalContext.current
+    val lightColorScheme = when (theme.colorMode) {
+        ColorMode.Dynamic -> dynamicLightColorScheme(context)
+        ColorMode.Custom -> LightCustomColorScheme
+        ColorMode.Baseline -> lightColorScheme()
+    }
+    val darkColorScheme = when (theme.colorMode) {
+        ColorMode.Dynamic -> dynamicDarkColorScheme(context)
+        ColorMode.Custom -> DarkCustomColorScheme
+        ColorMode.Baseline -> darkColorScheme()
+    }
+    val colorScheme = colorSchemeFromThemeMode(
+        themeMode = theme.themeMode,
+        lightColorScheme = lightColorScheme,
+        darkColorScheme = darkColorScheme
+    )
 
     val layoutDirection = when (theme.textDirection) {
         TextDirection.LTR -> LayoutDirection.Ltr
@@ -79,7 +74,6 @@
     }
 
     val view = LocalView.current
-    val context = LocalContext.current
     val darkTheme = isSystemInDarkTheme()
     SideEffect {
         WindowCompat.getInsetsController(context.findActivity().window, view)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/ThemePicker.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/ThemePicker.kt
index 3b5c9bf..636426b 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/ThemePicker.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/theme/ThemePicker.kt
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.safeDrawing
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.selectable
 import androidx.compose.material3.Button
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.MaterialTheme
@@ -43,6 +44,7 @@
 import androidx.compose.material3.catalog.library.model.TextDirection
 import androidx.compose.material3.catalog.library.model.Theme
 import androidx.compose.material3.catalog.library.model.ThemeMode
+import androidx.compose.material3.minimumInteractiveComponentSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
@@ -51,6 +53,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -80,26 +83,25 @@
             val themeModes = ThemeMode.values()
             Column(
                 modifier = Modifier.padding(ThemePickerPadding),
-                verticalArrangement = Arrangement.spacedBy(ThemePickerPadding)
             ) {
                 Row(horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)) {
-                    ThemeModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        themeMode = themeModes[0],
+                        option = themeModes[0],
                         selected = themeModes[0] == theme.themeMode,
                         onClick = { onThemeChange(theme.copy(themeMode = it)) }
                     )
-                    ThemeModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        themeMode = themeModes[1],
+                        option = themeModes[1],
                         selected = themeModes[1] == theme.themeMode,
                         onClick = { onThemeChange(theme.copy(themeMode = it)) }
                     )
                 }
                 Row {
-                    ThemeModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        themeMode = themeModes[2],
+                        option = themeModes[2],
                         selected = themeModes[2] == theme.themeMode,
                         onClick = { onThemeChange(theme.copy(themeMode = it)) }
                     )
@@ -117,27 +119,27 @@
             val colorModes = ColorMode.values()
             Column(
                 modifier = Modifier.padding(ThemePickerPadding),
-                verticalArrangement = Arrangement.spacedBy(ThemePickerPadding)
             ) {
                 Row(horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)) {
-                    ColorModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        colorMode = colorModes[0],
+                        option = colorModes[0],
                         selected = colorModes[0] == theme.colorMode,
                         onClick = { onThemeChange(theme.copy(colorMode = it)) }
                     )
-                    ColorModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        colorMode = colorModes[1],
+                        option = colorModes[1],
                         selected = colorModes[1] == theme.colorMode,
                         onClick = { onThemeChange(theme.copy(colorMode = it)) }
                     )
                 }
                 Row {
-                    ColorModeItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        colorMode = colorModes[2],
+                        option = colorModes[2],
                         selected = colorModes[2] == theme.colorMode,
+                        enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
                         onClick = { onThemeChange(theme.copy(colorMode = it)) }
                     )
                 }
@@ -154,26 +156,25 @@
             val textDirections = TextDirection.values()
             Column(
                 modifier = Modifier.padding(ThemePickerPadding),
-                verticalArrangement = Arrangement.spacedBy(ThemePickerPadding)
             ) {
                 Row(horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)) {
-                    TextDirectionItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        textDirection = textDirections[0],
+                        option = textDirections[0],
                         selected = textDirections[0] == theme.textDirection,
                         onClick = { onThemeChange(theme.copy(textDirection = it)) }
                     )
-                    TextDirectionItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        textDirection = textDirections[1],
+                        option = textDirections[1],
                         selected = textDirections[1] == theme.textDirection,
                         onClick = { onThemeChange(theme.copy(textDirection = it)) }
                     )
                 }
                 Row {
-                    TextDirectionItem(
+                    RadioButtonOption(
                         modifier = Modifier.weight(1f),
-                        textDirection = textDirections[2],
+                        option = textDirections[2],
                         selected = textDirections[2] == theme.textDirection,
                         onClick = { onThemeChange(theme.copy(textDirection = it)) }
                     )
@@ -187,44 +188,27 @@
                 style = MaterialTheme.typography.bodyMedium,
                 modifier = Modifier.padding(horizontal = ThemePickerPadding)
             )
-            Column {
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)
-                ) {
-                    RadioButton(
-                        selected = theme.fontScaleMode == FontScaleMode.System,
-                        onClick = {
-                            onThemeChange(theme.copy(fontScaleMode = FontScaleMode.System))
-                        },
-                    )
-                    Text(
-                        text = FontScaleMode.System.label,
-                        style = MaterialTheme.typography.bodyMedium
-                    )
-                }
+            Column(
+                modifier = Modifier.padding(ThemePickerPadding),
+            ) {
+                RadioButtonOption(
+                    option = FontScaleMode.System,
+                    selected = theme.fontScaleMode == FontScaleMode.System,
+                    onClick = {
+                        onThemeChange(theme.copy(fontScaleMode = FontScaleMode.System))
+                    }
+                )
+                RadioButtonOption(
+                    option = FontScaleMode.Custom,
+                    selected = theme.fontScaleMode == FontScaleMode.Custom,
+                    onClick = {
+                        onThemeChange(theme.copy(fontScaleMode = FontScaleMode.Custom))
+                    }
+                )
 
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)
-                ) {
-                    RadioButton(
-                        selected = theme.fontScaleMode == FontScaleMode.Custom,
-                        onClick = {
-                            onThemeChange(theme.copy(fontScaleMode = FontScaleMode.Custom))
-                        },
-                    )
-                    Text(
-                        text = FontScaleMode.Custom.label,
-                        style = MaterialTheme.typography.bodyMedium
-                    )
-                }
-
-                var fontScale by remember { mutableFloatStateOf(theme.fontScale) }
-                FontScaleItem(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .padding(horizontal = ThemePickerPadding),
+                var fontScale by remember(theme.fontScale) { mutableFloatStateOf(theme.fontScale) }
+                CustomFontScaleSlider(
+                    modifier = Modifier.fillMaxWidth(),
                     enabled = theme.fontScaleMode == FontScaleMode.Custom,
                     fontScale = fontScale,
                     onValueChange = { fontScale = it },
@@ -248,81 +232,39 @@
 }
 
 @Composable
-private fun ThemeModeItem(
-    modifier: Modifier = Modifier,
-    themeMode: ThemeMode,
+private fun <T> RadioButtonOption(
+    option: T,
     selected: Boolean,
-    onClick: (ThemeMode) -> Unit
+    onClick: (T) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
 ) {
     Row(
-        modifier = modifier,
+        modifier = modifier
+            .selectable(
+                selected = selected,
+                enabled = enabled,
+                onClick = { onClick(option) },
+                role = Role.RadioButton,
+            )
+            .minimumInteractiveComponentSize(),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)
     ) {
         RadioButton(
             selected = selected,
-            onClick = { onClick(themeMode) },
-        )
-        Text(
-            text = themeMode.toString(),
-            style = MaterialTheme.typography.bodyMedium
-        )
-    }
-}
-
-@Composable
-private fun ColorModeItem(
-    modifier: Modifier = Modifier,
-    colorMode: ColorMode,
-    selected: Boolean,
-    onClick: (ColorMode) -> Unit
-) {
-    Row(
-        modifier = modifier,
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)
-    ) {
-        val enabled = when {
-            colorMode == ColorMode.Dynamic && Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> false
-            else -> true
-        }
-        RadioButton(
-            selected = selected,
             enabled = enabled,
-            onClick = { onClick(colorMode) },
+            onClick = null,
         )
         Text(
-            text = colorMode.label,
+            text = option.toString(),
             style = MaterialTheme.typography.bodyMedium
         )
     }
 }
 
 @Composable
-private fun TextDirectionItem(
-    modifier: Modifier = Modifier,
-    textDirection: TextDirection,
-    selected: Boolean,
-    onClick: (TextDirection) -> Unit
-) {
-    Row(
-        modifier = modifier,
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(ThemePickerPadding)
-    ) {
-        RadioButton(
-            selected = selected,
-            onClick = { onClick(textDirection) },
-        )
-        Text(
-            text = textDirection.toString(),
-            style = MaterialTheme.typography.bodyMedium
-        )
-    }
-}
-
-@Composable
-private fun FontScaleItem(
+private fun CustomFontScaleSlider(
     modifier: Modifier = Modifier,
     enabled: Boolean,
     fontScale: Float,
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/res/values/donottranslate-strings.xml b/compose/material3/material3/integration-tests/material3-catalog/src/main/res/values/donottranslate-strings.xml
index df683d3..efb89e9 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/res/values/donottranslate-strings.xml
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/res/values/donottranslate-strings.xml
@@ -19,6 +19,11 @@
 
     <string name="compose_material_3">Compose Material 3</string>
 
+    <string name="back_button">Back</string>
+    <string name="favorite_button">Pin screen</string>
+    <string name="change_theme_button">Change theme</string>
+    <string name="more_menu_button">External links</string>
+
     <string name="description">Description</string>
     <string name="examples">Examples</string>
     <string name="no_examples">No examples</string>
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
index 20b1373..792b4a6 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
@@ -20,13 +20,14 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.material.Slider
+import androidx.compose.foundation.layout.width
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ProgressIndicatorDefaults
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -36,8 +37,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 
@@ -56,21 +55,13 @@
             progress = { animatedProgress },
         )
         Spacer(Modifier.requiredHeight(30.dp))
+        Text("Set progress:")
         Slider(
-            modifier = Modifier
-                .padding(start = 24.dp, end = 24.dp)
-                .semantics {
-                    val progressPercent = (progress * 100).toInt()
-                    if (progressPercent in progressBreakpoints) {
-                        stateDescription = "Progress $progressPercent%"
-                    }
-                },
+            modifier = Modifier.width(300.dp),
             value = progress,
             valueRange = 0f..1f,
-            steps = 100,
-            onValueChange = {
-                progress = it
-            })
+            onValueChange = { progress = it },
+        )
     }
 }
 
@@ -92,21 +83,13 @@
             drawStopIndicator = null
         )
         Spacer(Modifier.requiredHeight(30.dp))
+        Text("Set progress:")
         Slider(
-            modifier = Modifier
-                .padding(start = 24.dp, end = 24.dp)
-                .semantics {
-                    val progressPercent = (progress * 100).toInt()
-                    if (progressPercent in progressBreakpoints) {
-                        stateDescription = "Progress $progressPercent%"
-                    }
-                },
+            modifier = Modifier.width(300.dp),
             value = progress,
             valueRange = 0f..1f,
-            steps = 100,
-            onValueChange = {
-                progress = it
-            })
+            onValueChange = { progress = it },
+        )
     }
 }
 
@@ -144,21 +127,13 @@
     Column(horizontalAlignment = Alignment.CenterHorizontally) {
         CircularProgressIndicator(progress = { animatedProgress })
         Spacer(Modifier.requiredHeight(30.dp))
+        Text("Set progress:")
         Slider(
-            modifier = Modifier
-                .padding(start = 24.dp, end = 24.dp)
-                .semantics {
-                    val progressPercent = (progress * 100).toInt()
-                    if (progressPercent in progressBreakpoints) {
-                        stateDescription = "Progress $progressPercent%"
-                    }
-                },
+            modifier = Modifier.width(300.dp),
             value = progress,
             valueRange = 0f..1f,
-            steps = 100,
-            onValueChange = {
-                progress = it
-            })
+            onValueChange = { progress = it },
+        )
     }
 }
 
@@ -179,21 +154,13 @@
             gapSize = 0.dp
         )
         Spacer(Modifier.requiredHeight(30.dp))
+        Text("Set progress:")
         Slider(
-            modifier = Modifier
-                .padding(start = 24.dp, end = 24.dp)
-                .semantics {
-                    val progressPercent = (progress * 100).toInt()
-                    if (progressPercent in progressBreakpoints) {
-                        stateDescription = "Progress $progressPercent%"
-                    }
-                },
+            modifier = Modifier.width(300.dp),
             value = progress,
             valueRange = 0f..1f,
-            steps = 100,
-            onValueChange = {
-                progress = it
-            })
+            onValueChange = { progress = it }
+        )
     }
 }
 
@@ -215,5 +182,3 @@
         )
     }
 }
-
-private val progressBreakpoints = listOf(20, 40, 60, 80, 100)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
index eb71d36..427c034 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
@@ -21,11 +21,17 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.tokens.ElevatedButtonTokens
+import androidx.compose.material3.tokens.FilledButtonTokens
+import androidx.compose.material3.tokens.FilledTonalButtonTokens
+import androidx.compose.material3.tokens.OutlinedButtonTokens
+import androidx.compose.material3.tokens.TextButtonTokens
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -297,6 +303,119 @@
             "padding between end of text and end of text button."
         )
     }
+
+    @Test
+    fun button_defaultColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            assertThat(
+                ButtonDefaults.buttonColors(
+                    containerColor = Color.Unspecified,
+                    contentColor = Color.Unspecified,
+                    disabledContainerColor = Color.Unspecified,
+                    disabledContentColor = Color.Unspecified,
+                )
+            ).isEqualTo(
+                ButtonColors(
+                    containerColor = FilledButtonTokens.ContainerColor.value,
+                    contentColor = FilledButtonTokens.LabelTextColor.value,
+                    disabledContainerColor = FilledButtonTokens.DisabledContainerColor.value
+                        .copy(FilledButtonTokens.DisabledContainerOpacity),
+                    disabledContentColor = FilledButtonTokens.DisabledLabelTextColor.value
+                        .copy(alpha = FilledButtonTokens.DisabledLabelTextOpacity),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun filledTonalButton_defaultColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            assertThat(
+                ButtonDefaults.filledTonalButtonColors(
+                    containerColor = Color.Unspecified,
+                    contentColor = Color.Unspecified,
+                    disabledContainerColor = Color.Unspecified,
+                    disabledContentColor = Color.Unspecified,
+                )
+            ).isEqualTo(
+                ButtonColors(
+                    containerColor = FilledTonalButtonTokens.ContainerColor.value,
+                    contentColor = FilledTonalButtonTokens.LabelTextColor.value,
+                    disabledContainerColor = FilledTonalButtonTokens.DisabledContainerColor.value
+                        .copy(alpha = FilledTonalButtonTokens.DisabledContainerOpacity),
+                    disabledContentColor = FilledTonalButtonTokens.DisabledLabelTextColor.value
+                        .copy(alpha = FilledTonalButtonTokens.DisabledLabelTextOpacity),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun elevatedButton_defaultColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            assertThat(
+                ButtonDefaults.elevatedButtonColors(
+                    containerColor = Color.Unspecified,
+                    contentColor = Color.Unspecified,
+                    disabledContainerColor = Color.Unspecified,
+                    disabledContentColor = Color.Unspecified,
+                )
+            ).isEqualTo(
+                ButtonColors(
+                    containerColor = ElevatedButtonTokens.ContainerColor.value,
+                    contentColor = ElevatedButtonTokens.LabelTextColor.value,
+                    disabledContainerColor = ElevatedButtonTokens.DisabledContainerColor.value
+                        .copy(alpha = ElevatedButtonTokens.DisabledContainerOpacity),
+                    disabledContentColor = ElevatedButtonTokens.DisabledLabelTextColor.value
+                        .copy(alpha = ElevatedButtonTokens.DisabledLabelTextOpacity),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun outlinedButton_defaultColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            assertThat(
+                ButtonDefaults.outlinedButtonColors(
+                    containerColor = Color.Unspecified,
+                    contentColor = Color.Unspecified,
+                    disabledContainerColor = Color.Unspecified,
+                    disabledContentColor = Color.Unspecified,
+                )
+            ).isEqualTo(
+                ButtonColors(
+                    containerColor = Color.Transparent,
+                    contentColor = OutlinedButtonTokens.LabelTextColor.value,
+                    disabledContainerColor = Color.Transparent,
+                    disabledContentColor = OutlinedButtonTokens.DisabledLabelTextColor.value
+                        .copy(alpha = OutlinedButtonTokens.DisabledLabelTextOpacity),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun textButton_defaultColors() {
+        rule.setMaterialContent(lightColorScheme()) {
+            assertThat(
+                ButtonDefaults.textButtonColors(
+                    containerColor = Color.Unspecified,
+                    contentColor = Color.Unspecified,
+                    disabledContainerColor = Color.Unspecified,
+                    disabledContentColor = Color.Unspecified,
+                )
+            ).isEqualTo(
+                ButtonColors(
+                    containerColor = Color.Transparent,
+                    contentColor = TextButtonTokens.LabelTextColor.value,
+                    disabledContainerColor = Color.Transparent,
+                    disabledContentColor = TextButtonTokens.DisabledLabelTextColor.value
+                        .copy(alpha = TextButtonTokens.DisabledLabelTextOpacity),
+                )
+            )
+        }
+    }
 }
 
 private const val ButtonTestTag = "button"
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
index 6c1771f4..faa9850 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
@@ -1463,7 +1463,9 @@
             }
             TransformedText(transformed, mapping)
         }
-        rule.setMaterialContent(lightColorScheme()) {
+        // While surface is not used in TextField, setMaterialContent wraps content in a Surface
+        // component which is checked during assertPixels.
+        rule.setMaterialContent(lightColorScheme(surface = Color.White)) {
             TextField(
                 modifier = Modifier.testTag(TextFieldTag),
                 value = "",
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index f811d96..d7f6315 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -668,7 +668,7 @@
                 disabledContentColor = fromToken(FilledTonalButtonTokens.DisabledLabelTextColor)
                     .copy(alpha = FilledTonalButtonTokens.DisabledLabelTextOpacity)
             ).also {
-                defaultElevatedButtonColorsCached = it
+                defaultFilledTonalButtonColorsCached = it
             }
         }
 
@@ -732,12 +732,10 @@
      */
     @Composable
     fun textButtonColors(
-        containerColor: Color = Color.Transparent,
-        contentColor: Color = TextButtonTokens.LabelTextColor.value,
-        disabledContainerColor: Color = Color.Transparent,
-        disabledContentColor: Color = TextButtonTokens.DisabledLabelTextColor
-            .value
-            .copy(alpha = TextButtonTokens.DisabledLabelTextOpacity),
+        containerColor: Color = Color.Unspecified,
+        contentColor: Color = Color.Unspecified,
+        disabledContainerColor: Color = Color.Unspecified,
+        disabledContentColor: Color = Color.Unspecified,
     ): ButtonColors = MaterialTheme.colorScheme.defaultTextButtonColors.copy(
         containerColor = containerColor,
         contentColor = contentColor,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index d16ae0a..9875e1b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -341,17 +341,16 @@
         }
         val gapSizeFraction = adjustedGapSize / size.width.toDp()
 
-        if (firstLineHead.value - firstLineTail.value >= 0) {
-            if (firstLineTail.value > gapSizeFraction) {
-                val start = if (secondLineHead.value > gapSizeFraction) {
-                    secondLineHead.value + gapSizeFraction
-                } else {
-                    0f
-                }
-                drawLinearIndicator(
-                    start, firstLineTail.value - gapSizeFraction, trackColor, strokeWidth, strokeCap
-                )
-            }
+        // Track before line 1
+        if (firstLineHead.value < 1f - gapSizeFraction) {
+            val start = if (firstLineHead.value > 0) firstLineHead.value + gapSizeFraction else 0f
+            drawLinearIndicator(
+                start, 1f, trackColor, strokeWidth, strokeCap
+            )
+        }
+
+        // Line 1
+        if (firstLineHead.value - firstLineTail.value > 0) {
             drawLinearIndicator(
                 firstLineHead.value,
                 firstLineTail.value,
@@ -359,18 +358,19 @@
                 strokeWidth,
                 strokeCap,
             )
-            if (firstLineHead.value < 1f - gapSizeFraction) {
-                drawLinearIndicator(
-                    firstLineHead.value + gapSizeFraction, 1f, trackColor, strokeWidth, strokeCap
-                )
-            }
         }
-        if (secondLineHead.value - secondLineTail.value >= 0) {
-            if (secondLineTail.value > gapSizeFraction) {
-                drawLinearIndicator(
-                    0f, secondLineTail.value - gapSizeFraction, trackColor, strokeWidth, strokeCap
-                )
-            }
+
+        // Track between line 1 and line 2
+        if (firstLineTail.value > gapSizeFraction) {
+            val start = if (secondLineHead.value > 0) secondLineHead.value + gapSizeFraction else 0f
+            val end = if (firstLineTail.value < 1f) firstLineTail.value - gapSizeFraction else 1f
+            drawLinearIndicator(
+                start, end, trackColor, strokeWidth, strokeCap
+            )
+        }
+
+        // Line 2
+        if (secondLineHead.value - secondLineTail.value > 0) {
             drawLinearIndicator(
                 secondLineHead.value,
                 secondLineTail.value,
@@ -378,16 +378,14 @@
                 strokeWidth,
                 strokeCap,
             )
-            if (secondLineHead.value < 1f - gapSizeFraction) {
-                val end = if (firstLineTail.value < 1f - gapSizeFraction) {
-                    firstLineTail.value - gapSizeFraction
-                } else {
-                    1f
-                }
-                drawLinearIndicator(
-                    secondLineHead.value + gapSizeFraction, end, trackColor, strokeWidth, strokeCap
-                )
-            }
+        }
+
+        // Track after line 2
+        if (secondLineTail.value > gapSizeFraction) {
+            val end = if (secondLineTail.value < 1) secondLineTail.value - gapSizeFraction else 1f
+            drawLinearIndicator(
+                0f, end, trackColor, strokeWidth, strokeCap
+            )
         }
     }
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index e32df0b..cffb68d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -1111,12 +1111,12 @@
      * accessibility services.
      */
     @Deprecated(
-        message = "Use the overload that takes `thumbTrackGapSize`, `trackInsideCornerSize` and " +
-            "`drawStopIndicator`, see `LegacySliderSample` on how to restore the previous " +
+        message = "Use the overload that takes `drawStopIndicator`, `thumbTrackGapSize` and " +
+            "`trackInsideCornerSize`, see `LegacySliderSample` on how to restore the previous " +
             "behavior",
         replaceWith = ReplaceWith(
-            "Track(sliderState, modifier, colors, enabled, thumbTrackGapSize, " +
-                "trackInsideCornerSize, drawStopIndicator)"
+            "Track(sliderState, modifier, enabled, colors, drawStopIndicator, " +
+                "thumbTrackGapSize, trackInsideCornerSize)"
         ),
         level = DeprecationLevel.HIDDEN
     )
@@ -1131,8 +1131,8 @@
         Track(
             sliderState,
             modifier,
-            colors,
             enabled,
+            colors,
             thumbTrackGapSize = ThumbTrackGapSize,
             trackInsideCornerSize = TrackInsideCornerSize
         )
@@ -1143,32 +1143,32 @@
      *
      * @param sliderState [SliderState] which is used to obtain the current active track.
      * @param modifier the [Modifier] to be applied to the track.
-     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
-     * different states. See [SliderDefaults.colors].
      * @param enabled controls the enabled state of this slider. When `false`, this component will
      * not respond to user input, and it will appear visually disabled and disabled to
      * accessibility services.
-     * @param thumbTrackGapSize size of the gap between the thumb and the track.
-     * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
+     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
+     * different states. See [SliderDefaults.colors].
      * @param drawStopIndicator lambda that will be called to draw the stop indicator at the end of
      * the track.
+     * @param thumbTrackGapSize size of the gap between the thumb and the track.
+     * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
      */
     @ExperimentalMaterial3Api
     @Composable
     fun Track(
         sliderState: SliderState,
         modifier: Modifier = Modifier,
-        colors: SliderColors = colors(),
         enabled: Boolean = true,
-        thumbTrackGapSize: Dp = ThumbTrackGapSize,
-        trackInsideCornerSize: Dp = TrackInsideCornerSize,
+        colors: SliderColors = colors(),
         drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
             drawStopIndicator(
                 offset = it,
                 color = colors.activeTrackColor,
                 size = TrackStopIndicatorSize
             )
-        }
+        },
+        thumbTrackGapSize: Dp = ThumbTrackGapSize,
+        trackInsideCornerSize: Dp = TrackInsideCornerSize
     ) {
         val inactiveTrackColor = colors.trackColor(enabled, active = false)
         val activeTrackColor = colors.trackColor(enabled, active = true)
@@ -1210,12 +1210,12 @@
      * accessibility services.
      */
     @Deprecated(
-        message = "Use the overload that takes `thumbTrackGapSize`, `trackInsideCornerSize` and " +
-            "`drawStopIndicator`, see `LegacyRangeSliderSample` on how to restore the " +
+        message = "Use the overload that takes `drawStopIndicator`, `thumbTrackGapSize` and " +
+            "`trackInsideCornerSize`, see `LegacyRangeSliderSample` on how to restore the " +
             "previous behavior",
         replaceWith = ReplaceWith(
-            "Track(rangeSliderState, modifier, colors, enabled, thumbTrackGapSize, " +
-                "trackInsideCornerSize, drawStopIndicator)"
+            "Track(rangeSliderState, modifier, colors, enabled, drawStopIndicator, " +
+                "thumbTrackGapSize, trackInsideCornerSize)"
         ),
         level = DeprecationLevel.HIDDEN
     )
@@ -1230,8 +1230,8 @@
         Track(
             rangeSliderState,
             modifier,
-            colors,
             enabled,
+            colors,
             thumbTrackGapSize = ThumbTrackGapSize,
             trackInsideCornerSize = TrackInsideCornerSize
         )
@@ -1242,32 +1242,32 @@
      *
      * @param rangeSliderState [RangeSliderState] which is used to obtain the current active track.
      * @param modifier the [Modifier] to be applied to the track.
-     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
-     * different states. See [SliderDefaults.colors].
      * @param enabled controls the enabled state of this slider. When `false`, this component will
      * not respond to user input, and it will appear visually disabled and disabled to
      * accessibility services.
-     * @param thumbTrackGapSize size of the gap between the thumbs and the track.
-     * @param trackInsideCornerSize size of the corners towards the thumbs when a gap is set.
+     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
+     * different states. See [SliderDefaults.colors].
      * @param drawStopIndicator lambda that will be called to draw the stop indicator at the
      * start/end of the track.
+     * @param thumbTrackGapSize size of the gap between the thumbs and the track.
+     * @param trackInsideCornerSize size of the corners towards the thumbs when a gap is set.
      */
     @OptIn(ExperimentalMaterial3Api::class)
     @Composable
     fun Track(
         rangeSliderState: RangeSliderState,
         modifier: Modifier = Modifier,
-        colors: SliderColors = colors(),
         enabled: Boolean = true,
-        thumbTrackGapSize: Dp = ThumbTrackGapSize,
-        trackInsideCornerSize: Dp = TrackInsideCornerSize,
+        colors: SliderColors = colors(),
         drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
             drawStopIndicator(
                 offset = it,
                 color = colors.activeTrackColor,
                 size = TrackStopIndicatorSize
             )
-        }
+        },
+        thumbTrackGapSize: Dp = ThumbTrackGapSize,
+        trackInsideCornerSize: Dp = TrackInsideCornerSize
     ) {
         val inactiveTrackColor = colors.trackColor(enabled, active = false)
         val activeTrackColor = colors.trackColor(enabled, active = true)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
index 230540c..99508c6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_162
+// VERSION: v0_210
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
 
 internal object ColorDarkTokens {
-    val Background = PaletteTokens.Neutral10
+    val Background = PaletteTokens.Neutral6
     val Error = PaletteTokens.Error80
     val ErrorContainer = PaletteTokens.Error30
     val InverseOnSurface = PaletteTokens.Neutral20
@@ -53,7 +53,7 @@
     val SecondaryContainer = PaletteTokens.Secondary30
     val SecondaryFixed = PaletteTokens.Secondary90
     val SecondaryFixedDim = PaletteTokens.Secondary80
-    val Surface = PaletteTokens.Neutral10
+    val Surface = PaletteTokens.Neutral6
     val SurfaceBright = PaletteTokens.Neutral24
     val SurfaceContainer = PaletteTokens.Neutral12
     val SurfaceContainerHigh = PaletteTokens.Neutral17
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
index cf18f07..7182813 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_162
+// VERSION: v0_210
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
 
 internal object ColorLightTokens {
-    val Background = PaletteTokens.Neutral99
+    val Background = PaletteTokens.Neutral98
     val Error = PaletteTokens.Error40
     val ErrorContainer = PaletteTokens.Error90
     val InverseOnSurface = PaletteTokens.Neutral95
@@ -53,7 +53,7 @@
     val SecondaryContainer = PaletteTokens.Secondary90
     val SecondaryFixed = PaletteTokens.Secondary90
     val SecondaryFixedDim = PaletteTokens.Secondary80
-    val Surface = PaletteTokens.Neutral99
+    val Surface = PaletteTokens.Neutral98
     val SurfaceBright = PaletteTokens.Neutral98
     val SurfaceContainer = PaletteTokens.Neutral94
     val SurfaceContainerHigh = PaletteTokens.Neutral92
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 9b26e4b4..549bd5f 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -338,6 +338,10 @@
   }
 
   public final class HotReloaderKt {
+    method @org.jetbrains.annotations.TestOnly public static void clearCompositionErrors();
+    method @org.jetbrains.annotations.TestOnly public static java.util.List<kotlin.Pair<java.lang.Exception,java.lang.Boolean>> currentCompositionErrors();
+    method @org.jetbrains.annotations.TestOnly public static void invalidateGroupsWithKey(int key);
+    method @org.jetbrains.annotations.TestOnly public static void simulateHotReload(Object context);
   }
 
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Immutable {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index e8028b1..b41bbb2 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -366,6 +366,10 @@
   }
 
   public final class HotReloaderKt {
+    method @org.jetbrains.annotations.TestOnly public static void clearCompositionErrors();
+    method @org.jetbrains.annotations.TestOnly public static java.util.List<kotlin.Pair<java.lang.Exception,java.lang.Boolean>> currentCompositionErrors();
+    method @org.jetbrains.annotations.TestOnly public static void invalidateGroupsWithKey(int key);
+    method @org.jetbrains.annotations.TestOnly public static void simulateHotReload(Object context);
   }
 
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Immutable {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/HotReloader.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/HotReloader.kt
index c9683a8..576b29e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/HotReloader.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/HotReloader.kt
@@ -61,19 +61,31 @@
 }
 
 /**
- * @suppress
+ * Simulates hot reload of all current compositions by disposing all composed content and restarting
+ * compositions. Calling this method switches recomposer into hot reload mode.
+ * Test-only API, not for use in production.
+ *
+ * @param context context for disposal.
  */
 @TestOnly
 fun simulateHotReload(context: Any) = HotReloader.simulateHotReload(context)
 
 /**
- * @suppress
+ * Invalidates composed groups with the given key. Calling this method switches recomposer into hot
+ * reload mode.
+ * Test-only API, not for use in production.
+ *
+ * @param key group key to invalidate.
  */
 @TestOnly
 fun invalidateGroupsWithKey(key: Int) = HotReloader.invalidateGroupsWithKey(key)
 
 /**
- * @suppress
+ * Get list of errors captured in composition. This list is only available when recomposer is in
+ * hot reload mode.
+ * Test-only API, not for use in production.
+ *
+ * @return pair of error and whether the error is recoverable.
  */
 // suppressing for test-only api
 @Suppress("ListIterator")
@@ -83,7 +95,8 @@
         .map { it.cause to it.recoverable }
 
 /**
- * @suppress
+ * Clears current composition errors in hot reload mode.
+ * Test-only API, not for use in production.
  */
 @TestOnly
 fun clearCompositionErrors() = HotReloader.clearErrors()
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index a9eff8a..544ba58 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2814,6 +2814,7 @@
     method public float getTouchSlop();
     property public long doubleTapMinTimeMillis;
     property public long doubleTapTimeoutMillis;
+    property public float handwritingSlop;
     property public long longPressTimeoutMillis;
     property public float maximumFlingVelocity;
     property public float touchSlop;
@@ -3054,12 +3055,14 @@
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ViewConfiguration {
     method public long getDoubleTapMinTimeMillis();
     method public long getDoubleTapTimeoutMillis();
+    method public default float getHandwritingSlop();
     method public long getLongPressTimeoutMillis();
     method public default float getMaximumFlingVelocity();
     method public default long getMinimumTouchTargetSize();
     method public float getTouchSlop();
     property public abstract long doubleTapMinTimeMillis;
     property public abstract long doubleTapTimeoutMillis;
+    property public default float handwritingSlop;
     property public abstract long longPressTimeoutMillis;
     property public default float maximumFlingVelocity;
     property public default long minimumTouchTargetSize;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d95c62c..3832244 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2867,6 +2867,7 @@
     method public float getTouchSlop();
     property public long doubleTapMinTimeMillis;
     property public long doubleTapTimeoutMillis;
+    property public float handwritingSlop;
     property public long longPressTimeoutMillis;
     property public float maximumFlingVelocity;
     property public float touchSlop;
@@ -3112,12 +3113,14 @@
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ViewConfiguration {
     method public long getDoubleTapMinTimeMillis();
     method public long getDoubleTapTimeoutMillis();
+    method public default float getHandwritingSlop();
     method public long getLongPressTimeoutMillis();
     method public default float getMaximumFlingVelocity();
     method public default long getMinimumTouchTargetSize();
     method public float getTouchSlop();
     property public abstract long doubleTapMinTimeMillis;
     property public abstract long doubleTapTimeoutMillis;
+    property public default float handwritingSlop;
     property public abstract long longPressTimeoutMillis;
     property public default float maximumFlingVelocity;
     property public default long minimumTouchTargetSize;
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.android.kt
index 6691a3e..dc22e2a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.android.kt
@@ -16,6 +16,11 @@
 
 package androidx.compose.ui.platform
 
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.platform.AndroidViewConfigurationApi34.getScaledHandwritingSlop
+
 /**
  * A [ViewConfiguration] with Android's default configurations. Derived from
  * [android.view.ViewConfiguration]
@@ -35,6 +40,20 @@
     override val touchSlop: Float
         get() = viewConfiguration.scaledTouchSlop.toFloat()
 
+    override val handwritingSlop: Float
+        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            getScaledHandwritingSlop(viewConfiguration)
+        } else {
+            super.handwritingSlop
+        }
+
     override val maximumFlingVelocity: Float
         get() = viewConfiguration.scaledMaximumFlingVelocity.toFloat()
 }
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private object AndroidViewConfigurationApi34 {
+    @DoNotInline
+    fun getScaledHandwritingSlop(viewConfiguration: android.view.ViewConfiguration) =
+        viewConfiguration.scaledHandwritingSlop.toFloat()
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
index 2bd0b64..70ee1e8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
@@ -215,10 +215,10 @@
 
 private fun View.containsDescendant(other: View): Boolean {
     var viewParent = other.parent
-    do {
+    while (viewParent != null) {
         if (viewParent === this.parent) return true
         viewParent = viewParent.parent
-    } while (viewParent != null)
+    }
     return false
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
index be65629..cbf742c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
@@ -48,6 +48,12 @@
     val touchSlop: Float
 
     /**
+     * Distance in pixels a stylus touch can wander before we think the user is handwriting.
+     */
+    val handwritingSlop: Float
+        get() = 2f
+
+    /**
      * The minimum touch target size. If layout has reduced the pointer input bounds below this,
      * the touch target will be expanded evenly around the layout to ensure that it is at least
      * this big.
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultViewConfiguration.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultViewConfiguration.skiko.kt
index 04067d8..1847315 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultViewConfiguration.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultViewConfiguration.skiko.kt
@@ -31,4 +31,7 @@
 
     override val touchSlop: Float
         get() = with(density) { 18.dp.toPx() }
+
+    override val handwritingSlop: Float
+        get() = with(density) { 2.dp.toPx() }
 }
diff --git a/datastore/datastore-core/src/commonMain/kotlin/androidx/datastore/core/DataStoreImpl.kt b/datastore/datastore-core/src/commonMain/kotlin/androidx/datastore/core/DataStoreImpl.kt
index 9bc16b5..89f7160 100644
--- a/datastore/datastore-core/src/commonMain/kotlin/androidx/datastore/core/DataStoreImpl.kt
+++ b/datastore/datastore-core/src/commonMain/kotlin/androidx/datastore/core/DataStoreImpl.kt
@@ -60,6 +60,11 @@
      * result in deadlock.
      */
     initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
+    /**
+     * The handler of [CorruptionException]s when they are thrown during reads or writes. It
+     * produces the new data to replace the corrupted data on disk. By default it is a no-op which
+     * simply throws the exception and does not produce new data.
+     */
     private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler(),
     private val scope: CoroutineScope = CoroutineScope(ioDispatcher() + SupervisorJob())
 ) : DataStore<T> {
@@ -296,10 +301,22 @@
         }
         val (newState, acquiredLock) =
             if (requireLock) {
-                coordinator.lock { attemptRead(acquiredLock = true) to true }
+                coordinator.lock {
+                    try {
+                        readDataOrHandleCorruption(hasWriteFileLock = true)
+                    } catch (ex: Throwable) {
+                        ReadException<T>(ex, coordinator.getVersion())
+                    } to true
+                }
             } else {
                 coordinator.tryLock { locked ->
-                    attemptRead(locked) to locked
+                    try {
+                        readDataOrHandleCorruption(locked)
+                    } catch (ex: Throwable) {
+                        ReadException<T>(
+                            ex, if (locked) coordinator.getVersion() else cachedVersion
+                        )
+                    } to locked
                 }
             }
         if (acquiredLock) {
@@ -308,32 +325,6 @@
         return newState
     }
 
-    /**
-     * Caller is responsible to lock or tryLock, and pass the [acquiredLock] parameter to indicate
-     * if it has acquired lock.
-     */
-    private suspend fun attemptRead(acquiredLock: Boolean): State<T> {
-        // read version before file
-        val currentVersion = coordinator.getVersion()
-        // use current version if it has lock, otherwise use the older version between current and
-        // cached version, which guarantees correctness
-        val readVersion = if (acquiredLock) {
-            currentVersion
-        } else {
-            inMemoryCache.currentState.version
-        }
-        val readResult = runCatching { readDataFromFileOrDefault() }
-        return if (readResult.isSuccess) {
-            Data(
-                readResult.getOrThrow(),
-                readResult.getOrThrow().hashCode(),
-                readVersion
-            )
-        } else {
-            ReadException<T>(readResult.exceptionOrNull()!!, readVersion)
-        }
-    }
-
     // Caller is responsible for (try to) getting file lock. It reads from the file directly without
     // checking shared counter version and returns serializer default value if file is not found.
     private suspend fun readDataFromFileOrDefault(): T {
@@ -344,12 +335,11 @@
         transform: suspend (t: T) -> T,
         callerContext: CoroutineContext
     ): T = coordinator.lock {
-        val curData = readDataFromFileOrDefault()
-        val curDataAndHash = Data(curData, curData.hashCode(), /* unused */ version = 0)
-        val newData = withContext(callerContext) { transform(curData) }
+        val curData = readDataOrHandleCorruption(hasWriteFileLock = true)
+        val newData = withContext(callerContext) { transform(curData.value) }
 
         // Check that curData has not changed...
-        curDataAndHash.checkHashCode()
+        curData.checkHashCode()
 
         if (curData != newData) {
             writeData(newData, updateCache = true)
@@ -377,6 +367,64 @@
         return newVersion
     }
 
+    private suspend fun readDataOrHandleCorruption(hasWriteFileLock: Boolean): Data<T> {
+        try {
+            return if (hasWriteFileLock) {
+                val data = readDataFromFileOrDefault()
+                Data(data, data.hashCode(), version = coordinator.getVersion())
+            } else {
+                val preLockVersion = coordinator.getVersion()
+                coordinator.tryLock { locked ->
+                    val data = readDataFromFileOrDefault()
+                    val version = if (locked) coordinator.getVersion() else preLockVersion
+                    Data(
+                        data,
+                        data.hashCode(),
+                        version
+                    )
+                }
+            }
+        } catch (ex: CorruptionException) {
+            var newData: T = corruptionHandler.handleCorruption(ex)
+            var version: Int // initialized inside the try block
+
+            try {
+                doWithWriteFileLock(hasWriteFileLock) {
+                    // Confirms the file is still corrupted before overriding
+                    try {
+                        newData = readDataFromFileOrDefault()
+                        version = coordinator.getVersion()
+                    } catch (ignoredEx: CorruptionException) {
+                        version = writeData(newData, updateCache = true)
+                    }
+                }
+            } catch (writeEx: Throwable) {
+                // If we fail to write the handled data, add the new exception as a suppressed
+                // exception.
+                ex.addSuppressed(writeEx)
+                throw ex
+            }
+
+            // If we reach this point, we've successfully replaced the data on disk with newData.
+            return Data(newData, newData.hashCode(), version)
+        }
+    }
+
+    @OptIn(ExperimentalContracts::class)
+    private suspend fun <R> doWithWriteFileLock(
+        hasWriteFileLock: Boolean,
+        block: suspend () -> R
+    ): R {
+        contract {
+            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+        }
+        return if (hasWriteFileLock) {
+            block()
+        } else {
+            coordinator.lock { block() }
+        }
+    }
+
     private inner class InitDataStore(
         initTasksList: List<suspend (api: InitializerApi<T>) -> Unit>
     ) : RunOnce() {
@@ -431,65 +479,6 @@
             }
             inMemoryCache.tryUpdate(initData)
         }
-
-        @OptIn(ExperimentalContracts::class)
-        private suspend fun <R> doWithWriteFileLock(
-            hasWriteFileLock: Boolean,
-            block: suspend () -> R
-        ): R {
-            contract {
-                callsInPlace(block, InvocationKind.EXACTLY_ONCE)
-            }
-            return if (hasWriteFileLock) {
-                block()
-            } else {
-                coordinator.lock { block() }
-            }
-        }
-
-        // Only be called from `readAndInit`. State is UnInitialized or ReadException.
-        private suspend fun readDataOrHandleCorruption(hasWriteFileLock: Boolean): Data<T> {
-            try {
-                return if (hasWriteFileLock) {
-                    val data = readDataFromFileOrDefault()
-                    Data(data, data.hashCode(), version = coordinator.getVersion())
-                } else {
-                    val preLockVersion = coordinator.getVersion()
-                    coordinator.tryLock { locked ->
-                        val data = readDataFromFileOrDefault()
-                        val version = if (locked) coordinator.getVersion() else preLockVersion
-                        Data(
-                            data,
-                            data.hashCode(),
-                            version
-                        )
-                    }
-                }
-            } catch (ex: CorruptionException) {
-                var newData: T = corruptionHandler.handleCorruption(ex)
-                var version: Int // initialized inside the try block
-
-                try {
-                    doWithWriteFileLock(hasWriteFileLock) {
-                        // Confirms the file is still corrupted before overriding
-                        try {
-                            newData = readDataFromFileOrDefault()
-                            version = coordinator.getVersion()
-                        } catch (ignoredEx: CorruptionException) {
-                            version = writeData(newData, updateCache = true)
-                        }
-                    }
-                } catch (writeEx: Throwable) {
-                    // If we fail to write the handled data, add the new exception as a suppressed
-                    // exception.
-                    ex.addSuppressed(writeEx)
-                    throw ex
-                }
-
-                // If we reach this point, we've successfully replaced the data on disk with newData.
-                return Data(newData, newData.hashCode(), version)
-            }
-        }
     }
 
     companion object {
diff --git a/datastore/datastore-core/src/commonTest/kotlin/androidx/datastore/core/SingleProcessDataStoreTest.kt b/datastore/datastore-core/src/commonTest/kotlin/androidx/datastore/core/SingleProcessDataStoreTest.kt
index 9db7724..92f53a3 100644
--- a/datastore/datastore-core/src/commonTest/kotlin/androidx/datastore/core/SingleProcessDataStoreTest.kt
+++ b/datastore/datastore-core/src/commonTest/kotlin/androidx/datastore/core/SingleProcessDataStoreTest.kt
@@ -21,14 +21,12 @@
 import androidx.datastore.TestingSerializerConfig
 import androidx.datastore.core.UpdatingDataContextElement.Companion.NESTED_UPDATE_ERROR_MESSAGE
 import androidx.datastore.core.handlers.NoOpCorruptionHandler
-import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
 import kotlin.coroutines.AbstractCoroutineContextElement
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.test.BeforeTest
-import kotlin.test.Ignore
 import kotlin.test.Test
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
@@ -1098,35 +1096,36 @@
         assertThat(store2.data.first()).isEqualTo(2.toByte())
     }
 
-    @Ignore // b/289582516
     @Test
     fun testReadHandlesCorruptionAfterInit() {
         runTest {
+            val corruptionHandler = TestingCorruptionHandler(replaceWith = 3.toByte())
             val newStore = newDataStore(
                 testFile,
-                corruptionHandler = ReplaceFileCorruptionHandler { _ -> 2 }
+                corruptionHandler = corruptionHandler
             )
 
             // create the file to prevent serializer from returning default value
             newStore.updateData { 1 }
             assertThat(newStore.data.first()).isEqualTo(1)
 
-            // increment version to force non-cached read which gets [CorruptionException], the
-            // current state is [Data]
+            // increment version to force non-cached read which gets automatically reset from a
+            // [CorruptionException], the current state is [Data]
             serializerConfig.failReadWithCorruptionException = true
             serializerConfig.defaultValue = 2
             newStore.incrementSharedCounter()
-            assertThat(newStore.data.first()).isEqualTo(2)
+            assertThat(newStore.data.first()).isEqualTo(3.toByte())
+            assertThat(corruptionHandler.numCalls.get()).isEqualTo(1)
         }
     }
 
-    @Ignore // b/289582516
     @Test
     fun testUpdateHandlesCorruptionAfterInit() {
         runTest {
+            val corruptionHandler = TestingCorruptionHandler(replaceWith = 3.toByte())
             val newStore = newDataStore(
                 testFile,
-                corruptionHandler = ReplaceFileCorruptionHandler { _ -> 2 }
+                corruptionHandler = corruptionHandler
             )
 
             // create the file to prevent serializer from returning default value
@@ -1138,7 +1137,8 @@
             serializerConfig.failReadWithCorruptionException = true
             serializerConfig.defaultValue = 2
             newStore.incrementSharedCounter()
-            assertThat(newStore.updateData { 3.toByte() }).isEqualTo(3)
+            assertThat(newStore.updateData { it.inc() }).isEqualTo(4)
+            assertThat(corruptionHandler.numCalls.get()).isEqualTo(1)
         }
     }
 
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index f27893a..7100dc8 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -5,7 +5,11 @@
 
 def docsForOptionalProject(path) {
     Project project = findProject(path)
-    if (project != null) docs(project)
+    if (project != null) {
+        dependencies {
+            docs(project)
+        }
+    }
 }
 
 android {
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index 1ead3d2..5dcec3d 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -43,15 +43,19 @@
 
 Soft removals **must** do the following:
 
-*   Mark the API as deprecated for at least one stable release prior to removal.
-*   In Java sources, mark the API with a `@RestrictTo(LIBRARY_GROUP_PREFIX)`
-    Java annotation as well as a `@removed <reason>` docs annotation explaining
-    why the API was removed.
-*   In Kotlin sources, mark the API with `@Deprecated(message = <reason>,
-    level = DeprecationLevel.HIDDEN)` explaining why the API was removed.
-*   Maintain binary compatibility, as the API may still be called by existing
-    dependent libraries.
-*   Maintain behavioral compatibility and existing tests.
+1.  Mark the API as deprecated for at least one stable release prior to removal,
+    following language conventions for documenting why the API is being removed.
+1.  In a subsequent release:
+    *   In Java sources, mark the API as `@RestrictTo(LIBRARY_GROUP_PREFIX)`.
+        This will remove the API from `current.txt` but retain it in
+        `restricted_current.txt` for compatibility checking.
+    *   In Kotlin sources, mark the API as `@Deprecated(message = <reason>,
+        level = DeprecationLevel.HIDDEN)`. This will retain the API in
+        `current.txt` for compatibility checking.
+1.  For all future releases:
+    *   Maintain binary compatibility, as the API may still be called by
+        existing dependent libraries.
+    *   Maintain behavioral compatibility and existing tests.
 
 This is a disruptive change and should be avoided when possible.
 
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 776449b6..bbcc50a 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -37,7 +37,6 @@
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Log;
-import android.util.Pair;
 
 import androidx.annotation.RequiresApi;
 import androidx.exifinterface.test.R;
@@ -67,7 +66,6 @@
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.Objects;
 import java.util.Random;
 import java.util.concurrent.TimeUnit;
@@ -94,111 +92,6 @@
     private static final double DELTA = 1e-8;
     // We translate double to rational in a 1/10000 precision.
     private static final double RATIONAL_DELTA = 0.0001;
-    private static final int[][] TEST_ROTATION_STATE_MACHINE = {
-            {ExifInterface.ORIENTATION_UNDEFINED, -90, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_UNDEFINED, 0, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_UNDEFINED, 90, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_UNDEFINED, 180, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_UNDEFINED, 270, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_UNDEFINED, 540, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_NORMAL, -90, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_NORMAL, 0, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_NORMAL, 90, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_NORMAL, 180, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_NORMAL, 270, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_NORMAL, 540, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_ROTATE_90, -90, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_ROTATE_90, 0, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_ROTATE_90, 90, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_ROTATE_90, 180 , ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_ROTATE_90, 270, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_ROTATE_90, 540, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_ROTATE_180, -90, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_ROTATE_180, 0, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_ROTATE_180, 90, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_ROTATE_180, 180, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_ROTATE_180, 270, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_ROTATE_180, 540, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_ROTATE_270, -90, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_ROTATE_270, 0, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_ROTATE_270, 90, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_ROTATE_270, 180, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_ROTATE_270, 270, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_ROTATE_270, 540, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, -90, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, 0, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, 90, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, 180,
-                    ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, 270, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, 540,
-                    ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, -90, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 0,
-                    ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 90, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 180,
-                    ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 270, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 540,
-                    ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_TRANSPOSE, -90, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_TRANSPOSE, 0, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_TRANSPOSE, 90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_TRANSPOSE, 180, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_TRANSPOSE, 270, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_TRANSPOSE, 540, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_TRANSVERSE, -90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_TRANSVERSE, 0, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_TRANSVERSE, 90, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_TRANSVERSE, 180, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_TRANSVERSE, 270, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_TRANSVERSE, 540, ExifInterface.ORIENTATION_TRANSPOSE},
-    };
-    private static final int[][] TEST_FLIP_VERTICALLY_STATE_MACHINE = {
-            {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_270},
-            {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_90}
-    };
-    private static final int[][] TEST_FLIP_HORIZONTALLY_STATE_MACHINE = {
-            {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED},
-            {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
-            {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSPOSE},
-            {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL},
-            {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSVERSE},
-            {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_ROTATE_180},
-            {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_NORMAL},
-            {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_90},
-            {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_270}
-    };
-    private static final HashMap<Integer, Pair<Boolean, Integer>> FLIP_STATE_AND_ROTATION_DEGREES =
-            new HashMap<>();
-    static {
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_UNDEFINED, new Pair<>(false, 0));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_NORMAL, new Pair<>(false, 0));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_ROTATE_90, new Pair<>(false, 90));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_ROTATE_180, new Pair<>(false, 180));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_ROTATE_270, new Pair<>(false, 270));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_FLIP_HORIZONTAL, new Pair<>(true, 0));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_TRANSVERSE, new Pair<>(true, 90));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_FLIP_VERTICAL, new Pair<>(true, 180));
-        FLIP_STATE_AND_ROTATION_DEGREES.put(
-                ExifInterface.ORIENTATION_TRANSPOSE, new Pair<>(true, 270));
-    }
 
     private static final String[] EXIF_TAGS = {
             ExifInterface.TAG_MAKE,
@@ -794,73 +687,371 @@
     @Test
     @LargeTest
     public void testRotation() throws IOException {
+        // Test flip vertically.
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                ExifInterface::flipVertically,
+                ExifInterface.ORIENTATION_ROTATE_90);
+
+        // Test flip horizontally.
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                ExifInterface::flipHorizontally,
+                ExifInterface.ORIENTATION_ROTATE_270);
+
+        // Test rotate by degrees
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_UNDEFINED,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_UNDEFINED);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_NORMAL,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_90,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_180,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_ROTATE_270);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_NORMAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_ROTATE_180);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_ROTATE_270,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_ROTATE_90);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_VERTICAL,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSPOSE,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(-90),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(0),
+                ExifInterface.ORIENTATION_TRANSVERSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(90),
+                ExifInterface.ORIENTATION_FLIP_VERTICAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(180),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(270),
+                ExifInterface.ORIENTATION_FLIP_HORIZONTAL);
+        testModifyOrientation(
+                ExifInterface.ORIENTATION_TRANSVERSE,
+                exifInterface -> exifInterface.rotate(540),
+                ExifInterface.ORIENTATION_TRANSPOSE);
+    }
+
+    private void testModifyOrientation(
+            int originalOrientation,
+            ExifInterfaceOperation rotationOperation,
+            int expectedOrientation)
+            throws IOException {
         File imageFile =
                 copyFromResourceToFile(
                         R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        exif.setAttribute(ExifInterface.TAG_ORIENTATION, Integer.toString(originalOrientation));
+        rotationOperation.applyTo(exif);
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertThat(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0))
+                .isEqualTo(expectedOrientation);
+        imageFile.delete();
+    }
 
-        int num;
-        // Test flip vertically.
-        for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) {
-            exif.setAttribute(ExifInterface.TAG_ORIENTATION,
-                    Integer.toString(TEST_FLIP_VERTICALLY_STATE_MACHINE[num][0]));
-            exif.flipVertically();
-            exif.saveAttributes();
-            exif = new ExifInterface(imageFile.getAbsolutePath());
-            assertIntTag(exif, ExifInterface.TAG_ORIENTATION,
-                    TEST_FLIP_VERTICALLY_STATE_MACHINE[num][1]);
+    @Test
+    @LargeTest
+    public void testRotation_byDegrees_invalid() throws IOException {
+        File imageFile =
+                copyFromResourceToFile(
+                        R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertThrows(IllegalArgumentException.class, () -> exif.rotate(108));
+    }
 
-        }
+    @Test
+    @LargeTest
+    public void testRotation_flipState() throws IOException {
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_UNDEFINED, false, 0);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_NORMAL, false, 0);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_ROTATE_90, false, 90);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_ROTATE_180, false, 180);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_ROTATE_270, false, 270);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL, true, 0);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_TRANSVERSE, true, 90);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_FLIP_VERTICAL, true, 180);
+        testFlipStateAndRotation(ExifInterface.ORIENTATION_TRANSPOSE, true, 270);
+    }
 
-        // Test flip horizontally.
-        for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) {
-            exif.setAttribute(ExifInterface.TAG_ORIENTATION,
-                    Integer.toString(TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][0]));
-            exif.flipHorizontally();
-            exif.saveAttributes();
-            exif = new ExifInterface(imageFile.getAbsolutePath());
-            assertIntTag(exif, ExifInterface.TAG_ORIENTATION,
-                    TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][1]);
+    private void testFlipStateAndRotation(
+            int orientation, boolean expectedFlipState, int expectedDegrees) throws IOException {
+        File imageFile =
+                copyFromResourceToFile(
+                        R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertThat(exif.isFlipped()).isEqualTo(expectedFlipState);
+        assertThat(exif.getRotationDegrees()).isEqualTo(expectedDegrees);
+        imageFile.delete();
+    }
 
-        }
-
-        // Test rotate by degrees
-        exif.setAttribute(ExifInterface.TAG_ORIENTATION,
-                Integer.toString(ExifInterface.ORIENTATION_NORMAL));
-        try {
-            exif.rotate(108);
-            fail("Rotate with 108 degree should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // Success
-        }
-
-        for (num = 0; num < TEST_ROTATION_STATE_MACHINE.length; num++) {
-            exif.setAttribute(ExifInterface.TAG_ORIENTATION,
-                    Integer.toString(TEST_ROTATION_STATE_MACHINE[num][0]));
-            exif.rotate(TEST_ROTATION_STATE_MACHINE[num][1]);
-            exif.saveAttributes();
-            exif = new ExifInterface(imageFile.getAbsolutePath());
-            assertIntTag(exif, ExifInterface.TAG_ORIENTATION, TEST_ROTATION_STATE_MACHINE[num][2]);
-        }
-
-        // Test get flip state and rotation degrees.
-        for (Integer key : FLIP_STATE_AND_ROTATION_DEGREES.keySet()) {
-            exif.setAttribute(ExifInterface.TAG_ORIENTATION, key.toString());
-            exif.saveAttributes();
-            exif = new ExifInterface(imageFile.getAbsolutePath());
-            assertEquals(FLIP_STATE_AND_ROTATION_DEGREES.get(key).first, exif.isFlipped());
-            assertEquals((int) FLIP_STATE_AND_ROTATION_DEGREES.get(key).second,
-                    exif.getRotationDegrees());
-        }
-
-        // Test reset the rotation.
-        exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+    @Test
+    @LargeTest
+    public void testResetOrientation() throws IOException {
+        File imageFile =
+                copyFromResourceToFile(
+                        R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        exif.setAttribute(
+                ExifInterface.TAG_ORIENTATION,
                 Integer.toString(ExifInterface.ORIENTATION_FLIP_HORIZONTAL));
         exif.resetOrientation();
         exif.saveAttributes();
         exif = new ExifInterface(imageFile.getAbsolutePath());
         assertIntTag(exif, ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
-
     }
 
     @SuppressWarnings("deprecation")
@@ -1379,4 +1570,14 @@
         }
         return file;
     }
+
+    /**
+     * An operation that can be applied to an {@link ExifInterface} instance.
+     *
+     * <p>We would use java.util.Consumer but it's not available before API 24, and there's no Guava
+     * equivalent.
+     */
+    private interface ExifInterfaceOperation {
+        void applyTo(ExifInterface exifInterface);
+    }
 }
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 3257f21..6632acb 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -1868,7 +1868,8 @@
      */
     public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
     /**
-     *  <p>Indicates the speed of GPS receiver movement.</p>
+     * Indicates the speed of GPS receiver movement. The units are indicated by {@link
+     * #TAG_GPS_SPEED_REF}.
      *
      *  <ul>
      *      <li>Tag = 13</li>
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index e1e08ca..918e133 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -71,6 +71,7 @@
             dependsOn(jvmMain)
             dependencies {
                 implementation("androidx.core:core-ktx:1.10.0")
+                implementation(project(":annotation:annotation-experimental"))
             }
         }
 
@@ -135,4 +136,4 @@
     inceptionYear = "2022"
     description = "create and render rounded polygonal shapes"
     metalavaK2UastEnabled = true
-}
\ No newline at end of file
+}
diff --git a/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
index b00a1ca..bb92c9c 100644
--- a/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
+++ b/javascriptengine/javascriptengine/api/aidlRelease/current/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
@@ -32,7 +32,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package org.chromium.android_webview.js_sandbox.common;
-/* @hide */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 interface IJsSandboxIsolateClient {
   void onTerminated(int status, String message) = 1;
   const int TERMINATE_UNKNOWN_ERROR = 1;
diff --git a/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
index 6574ae4..d1ca462 100644
--- a/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
+++ b/javascriptengine/javascriptengine/src/main/stableAidl/org/chromium/android_webview/js_sandbox/common/IJsSandboxIsolateClient.aidl
@@ -18,8 +18,8 @@
 
 /**
  * Callbacks for isolate events, not specific to evaluations.
- * @hide
  */
+ @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 interface IJsSandboxIsolateClient {
     // These crash codes may be generated on either the client or service side.
 
diff --git a/libraryversions.toml b/libraryversions.toml
index aa7b933..1eafd9b 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -100,7 +100,7 @@
 MEDIAROUTER = "1.7.0-rc01"
 METRICS = "1.0.0-beta02"
 NAVIGATION = "2.8.0-alpha04"
-PAGING = "3.3.0-alpha03"
+PAGING = "3.3.0-alpha04"
 PALETTE = "1.1.0-alpha01"
 PDF = "1.0.0-alpha01"
 PERCENTLAYOUT = "1.1.0-alpha01"
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 81e610e..a92bda3 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -23,6 +23,7 @@
  */
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
 import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
@@ -37,6 +38,10 @@
     linux()
     ios()
 
+    kotlin {
+        explicitApi = ExplicitApiMode.Strict
+    }
+
     defaultPlatform(PlatformIdentifier.JVM)
 
     sourceSets {
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
index 4798c50..f992081 100644
--- a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
+++ b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
@@ -20,8 +20,8 @@
 
 public actual class AtomicReference<V> actual constructor(value: V) {
     private val delegate = atomic(value)
-    public actual fun get() = delegate.value
-    public actual fun compareAndSet(expect: V, newValue: V) =
+    public actual fun get(): V = delegate.value
+    public actual fun compareAndSet(expect: V, newValue: V): Boolean =
         delegate.compareAndSet(expect, newValue)
 }
 
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index 959a1b3..bbbc6a5 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -8,6 +8,7 @@
 
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.konan.target.Family
 
@@ -22,6 +23,10 @@
     linux()
     ios()
 
+    kotlin {
+        explicitApi = ExplicitApiMode.Strict
+    }
+
     defaultPlatform(PlatformIdentifier.ANDROID)
 
     sourceSets {
diff --git a/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ReportFragment.android.kt b/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ReportFragment.android.kt
index 8bcd9d0..4fda823 100644
--- a/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ReportFragment.android.kt
+++ b/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ReportFragment.android.kt
@@ -27,7 +27,7 @@
  */
 @Suppress("DEPRECATION")
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-open class ReportFragment() : android.app.Fragment() {
+public open class ReportFragment() : android.app.Fragment() {
     private var processListener: ActivityInitializationListener? = null
 
     private fun dispatchCreate(listener: ActivityInitializationListener?) {
@@ -86,14 +86,14 @@
         }
     }
 
-    fun setProcessListener(processListener: ActivityInitializationListener?) {
+    public fun setProcessListener(processListener: ActivityInitializationListener?) {
         this.processListener = processListener
     }
 
-    interface ActivityInitializationListener {
-        fun onCreate()
-        fun onStart()
-        fun onResume()
+    public interface ActivityInitializationListener {
+        public fun onCreate()
+        public fun onStart()
+        public fun onResume()
     }
 
     // this class isn't inlined only because we need to add a proguard rule for it (b/142778206)
@@ -156,12 +156,12 @@
         }
     }
 
-    companion object {
+    public companion object {
         private const val REPORT_FRAGMENT_TAG =
             "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag"
 
         @JvmStatic
-        fun injectIfNeededIn(activity: Activity) {
+        public fun injectIfNeededIn(activity: Activity) {
             if (Build.VERSION.SDK_INT >= 29) {
                 // On API 29+, we can register for the correct Lifecycle callbacks directly
                 LifecycleCallbacks.registerIn(activity)
@@ -194,7 +194,7 @@
 
         @JvmStatic
         @get:JvmName("get")
-        val Activity.reportFragment: ReportFragment
+        public val Activity.reportFragment: ReportFragment
             get() {
                 return this.fragmentManager.findFragmentByTag(
                     REPORT_FRAGMENT_TAG
diff --git a/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ViewTreeLifecycleOwner.android.kt b/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ViewTreeLifecycleOwner.android.kt
index ba13168..777d86f 100644
--- a/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ViewTreeLifecycleOwner.android.kt
+++ b/lifecycle/lifecycle-runtime/src/androidMain/kotlin/androidx/lifecycle/ViewTreeLifecycleOwner.android.kt
@@ -33,7 +33,7 @@
  * @param lifecycleOwner LifecycleOwner representing the manager of the given view
  */
 @JvmName("set")
-fun View.setViewTreeLifecycleOwner(lifecycleOwner: LifecycleOwner?) {
+public fun View.setViewTreeLifecycleOwner(lifecycleOwner: LifecycleOwner?) {
     setTag(R.id.view_tree_lifecycle_owner, lifecycleOwner)
 }
 
@@ -46,7 +46,7 @@
  * of its ancestors
  */
 @JvmName("get")
-fun View.findViewTreeLifecycleOwner(): LifecycleOwner? {
+public fun View.findViewTreeLifecycleOwner(): LifecycleOwner? {
     return generateSequence(this) { currentView ->
         currentView.parent as? View
     }.mapNotNull { viewParent ->
diff --git a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
index 2857b82..34304f5 100644
--- a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
+++ b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
@@ -24,7 +24,7 @@
  * It is used by Fragments and Support Library Activities. You can also directly use it if you have
  * a custom LifecycleOwner.
  */
-expect open class LifecycleRegistry
+public expect open class LifecycleRegistry
 
 /**
  * Creates a new LifecycleRegistry for the given provider.
@@ -45,16 +45,16 @@
      *
      * @param event The event that was received
      */
-    open fun handleLifecycleEvent(event: Event)
+    public open fun handleLifecycleEvent(event: Event)
 
     /**
      * The number of observers.
      *
      * @return The number of observers.
      */
-    open val observerCount: Int
+    public open val observerCount: Int
 
-    companion object {
+    public companion object {
         /**
          * Creates a new LifecycleRegistry for the given provider, that doesn't check
          * that its methods are called on the threads other than main.
@@ -65,6 +65,6 @@
          */
         @JvmStatic
         @VisibleForTesting
-        fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry
+        public fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry
     }
 }
diff --git a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
index da23db5..87cdbd9 100644
--- a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
+++ b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
@@ -29,7 +29,7 @@
  * It is used by Fragments and Support Library Activities. You can also directly use it if you have
  * a custom LifecycleOwner.
  */
-actual open class LifecycleRegistry private constructor(
+public actual open class LifecycleRegistry private constructor(
     provider: LifecycleOwner,
     private val enforceMainThread: Boolean
 ) : Lifecycle() {
@@ -77,7 +77,7 @@
      *
      * @param provider The owner LifecycleOwner
      */
-    actual constructor(provider: LifecycleOwner) : this(provider, true)
+    public actual constructor(provider: LifecycleOwner) : this(provider, true)
 
     init {
         lifecycleOwner = WeakReference(provider)
@@ -90,7 +90,7 @@
      */
     @MainThread
     @Deprecated("Override [currentState].")
-    open fun markState(state: State) {
+    public open fun markState(state: State) {
         enforceMainThreadIfNeeded("markState")
         currentState = state
     }
@@ -119,7 +119,7 @@
      *
      * @param event The event that was received
      */
-    actual open fun handleLifecycleEvent(event: Event) {
+    public actual open fun handleLifecycleEvent(event: Event) {
         enforceMainThreadIfNeeded("handleLifecycleEvent")
         moveToState(event.targetState)
     }
@@ -237,7 +237,7 @@
      *
      * @return The number of observers.
      */
-    actual open val observerCount: Int
+    public actual open val observerCount: Int
         get() {
             enforceMainThreadIfNeeded("getObserverCount")
             return observerMap.size()
@@ -322,7 +322,7 @@
         }
     }
 
-    actual companion object {
+    public actual companion object {
         /**
          * Creates a new LifecycleRegistry for the given provider, that doesn't check
          * that its methods are called on the threads other than main.
@@ -333,7 +333,7 @@
          */
         @JvmStatic
         @VisibleForTesting
-        actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
+        public actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
             return LifecycleRegistry(owner, false)
         }
 
diff --git a/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
index 4a3f707..978ee7c 100644
--- a/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
+++ b/lifecycle/lifecycle-runtime/src/nativeMain/kotlin/androidx/lifecycle/LifecycleRegistry.native.kt
@@ -27,7 +27,7 @@
  * It is used by Fragments and Support Library Activities. You can also directly use it if you have
  * a custom LifecycleOwner.
  */
-actual open class LifecycleRegistry private constructor(
+public actual open class LifecycleRegistry private constructor(
     provider: LifecycleOwner,
     private val enforceMainThread: Boolean
 ) : Lifecycle() {
@@ -73,7 +73,7 @@
      *
      * @param provider The owner LifecycleOwner
      */
-    actual constructor(provider: LifecycleOwner) : this(provider, true)
+    public actual constructor(provider: LifecycleOwner) : this(provider, true)
 
     init {
         lifecycleOwner = WeakReference(provider)
@@ -103,7 +103,7 @@
      *
      * @param event The event that was received
      */
-    actual open fun handleLifecycleEvent(event: Event) {
+    public actual open fun handleLifecycleEvent(event: Event) {
         enforceMainThreadIfNeeded("handleLifecycleEvent")
         moveToState(event.targetState)
     }
@@ -222,7 +222,7 @@
      *
      * @return The number of observers.
      */
-    actual open val observerCount: Int
+    public actual open val observerCount: Int
         get() {
             enforceMainThreadIfNeeded("getObserverCount")
             return observerMap.size
@@ -332,7 +332,7 @@
         }
     }
 
-    actual companion object {
+    public actual companion object {
         /**
          * Creates a new LifecycleRegistry for the given provider, that doesn't check
          * that its methods are called on the threads other than main.
@@ -342,7 +342,7 @@
          * Another possible use-case for this method is JVM testing, when main thread is not present.
          */
         @VisibleForTesting
-        actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
+        public actual fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
             return LifecycleRegistry(owner, false)
         }
 
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index c9e0da4..4e7e205 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -41,8 +41,9 @@
                 AndroidManifestServiceExportedDetector.ISSUE,
                 BanParcelableUsage.ISSUE,
                 BanConcurrentHashMap.ISSUE,
-                BanHideAndSuppressTags.HIDE_ISSUE,
-                BanHideAndSuppressTags.SUPPRESS_ISSUE,
+                BanVisibilityDocTags.HIDE_ISSUE,
+                BanVisibilityDocTags.SUPPRESS_ISSUE,
+                BanVisibilityDocTags.REMOVED_ISSUE,
                 BanInappropriateExperimentalUsage.ISSUE,
                 BanInappropriateExperimentalUsage.NULL_ANNOTATION_GROUP_ISSUE,
                 BanInlineOptIn.ISSUE,
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanHideAndSuppressTags.kt b/lint-checks/src/main/java/androidx/build/lint/BanVisibilityDocTags.kt
similarity index 79%
rename from lint-checks/src/main/java/androidx/build/lint/BanHideAndSuppressTags.kt
rename to lint-checks/src/main/java/androidx/build/lint/BanVisibilityDocTags.kt
index 065cb58..d9c7875 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanHideAndSuppressTags.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanVisibilityDocTags.kt
@@ -28,10 +28,11 @@
 import org.jetbrains.uast.UDeclaration
 
 @Suppress("unused")
-class BanHideAndSuppressTags : Detector(), Detector.UastScanner {
+class BanVisibilityDocTags : Detector(), Detector.UastScanner {
     private val tagToIssue = mapOf(
         "@hide" to HIDE_ISSUE,
         "@suppress" to SUPPRESS_ISSUE,
+        "@removed" to REMOVED_ISSUE,
     )
 
     override fun getApplicableUastTypes() = listOf(UDeclaration::class.java)
@@ -62,7 +63,7 @@
             priority = 5,
             severity = Severity.ERROR,
             implementation = Implementation(
-                BanHideAndSuppressTags::class.java,
+                BanVisibilityDocTags::class.java,
                 Scope.JAVA_FILE_SCOPE
             )
         )
@@ -75,7 +76,20 @@
             priority = 5,
             severity = Severity.ERROR,
             implementation = Implementation(
-                BanHideAndSuppressTags::class.java,
+                BanVisibilityDocTags::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+        val REMOVED_ISSUE = Issue.create(
+            id = "BanRemovedTag",
+            briefDescription = "@removed is not allowed in Javadoc",
+            explanation = "Use of the @removed annotation in Javadoc is no longer allowed." +
+                " Please use @RestrictTo(LIBRARY_GROUP_PREFIX) instead.",
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                BanVisibilityDocTags::class.java,
                 Scope.JAVA_FILE_SCOPE
             )
         )
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanHideAndSuppressTagsTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanVisibilityDocTagsTest.kt
similarity index 63%
rename from lint-checks/src/test/java/androidx/build/lint/BanHideAndSuppressTagsTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/BanVisibilityDocTagsTest.kt
index 8cc0c56..c8c7827 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanHideAndSuppressTagsTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanVisibilityDocTagsTest.kt
@@ -21,9 +21,13 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class BanHideAndSuppressTagsTest : AbstractLintDetectorTest(
-    useDetector = BanHideAndSuppressTags(),
-    useIssues = listOf(BanHideAndSuppressTags.HIDE_ISSUE, BanHideAndSuppressTags.SUPPRESS_ISSUE),
+class BanVisibilityDocTagsTest : AbstractLintDetectorTest(
+    useDetector = BanVisibilityDocTags(),
+    useIssues = listOf(
+        BanVisibilityDocTags.HIDE_ISSUE,
+        BanVisibilityDocTags.SUPPRESS_ISSUE,
+        BanVisibilityDocTags.REMOVED_ISSUE,
+    ),
 ) {
 
     private val fileWithHideInJavadoc = java(
@@ -108,4 +112,48 @@
 
         check(*input).expect(expected)
     }
+
+    @Test
+    fun `Detection of removed tag`() {
+        val input = arrayOf(
+            kotlin(
+                """
+                    class Foo {
+                        /**
+                          * A previously useful function.
+                          * @removed
+                          **/
+                        fun foo() = Unit
+                    }
+                """.trimIndent()
+            ),
+            java(
+                """
+                    /**
+                      * Bar class
+                      * @removed don't use this
+                      */
+                    public class Bar {
+                        /** @removed */
+                        public void bar() {}
+                    }
+                """.trimIndent()
+            )
+        )
+
+        val expected = """
+            src/Bar.java:5: Error: @removed is not allowed in documentation [BanRemovedTag]
+            public class Bar {
+                         ~~~
+            src/Bar.java:7: Error: @removed is not allowed in documentation [BanRemovedTag]
+                public void bar() {}
+                            ~~~
+            src/Foo.kt:6: Error: @removed is not allowed in documentation [BanRemovedTag]
+                fun foo() = Unit
+                    ~~~
+            3 errors, 0 warnings
+        """.trimIndent()
+
+        check(*input).expect(expected)
+    }
 }
diff --git a/mediarouter/mediarouter/src/main/res/values-eu/strings.xml b/mediarouter/mediarouter/src/main/res/values-eu/strings.xml
index 669e17e..c11b400 100644
--- a/mediarouter/mediarouter/src/main/res/values-eu/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-eu/strings.xml
@@ -50,5 +50,5 @@
     <string name="mr_chooser_wifi_warning_description_car" msgid="2998902945608081567">"Ziurtatu beste gailua autoaren wifi-sare berera konektatuta dagoela"</string>
     <string name="mr_chooser_wifi_warning_description_unknown" msgid="3459891599800041449">"Ziurtatu beste gailua gailu honen wifi-sare berera konektatuta dagoela"</string>
     <string name="mr_chooser_wifi_learn_more" msgid="3799500840179081429"><a href="https://support.google.com/chromecast/?p=trouble-finding-devices">"Lortu informazio gehiago"</a></string>
-    <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"Ikasi edukia igortzen"</string>
+    <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"Lortu edukia igortzeko argibideak"</string>
 </resources>
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index 1b41906..f47c16d 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -423,15 +423,15 @@
         }
 
         val transition = if (inPredictiveBack) {
-            val transitionState = remember(backStackEntry) {
+            val transitionState by remember(backStackEntry) {
                 // The state returned here cannot be nullable cause it produces the input of the
                 // transitionSpec passed into the AnimatedContent and that must match the non-nullable
                 // scope exposed by the transitions on the NavHost and composable APIs.
-                SeekableTransitionState(backStackEntry)
+                mutableStateOf(SeekableTransitionState(backStackEntry))
             }
             LaunchedEffect(progress) {
                 val previousEntry = currentBackStack[currentBackStack.size - 2]
-                transitionState.seekTo(progress, previousEntry)
+                transitionState.snapTo(previousEntry, progress)
             }
             rememberTransition(transitionState, label = "entry")
         } else {
diff --git a/placeholder-tests/build.gradle.kts b/placeholder-tests/build.gradle
similarity index 88%
rename from placeholder-tests/build.gradle.kts
rename to placeholder-tests/build.gradle
index d442e25..20e7b60e 100644
--- a/placeholder-tests/build.gradle.kts
+++ b/placeholder-tests/build.gradle
@@ -27,6 +27,10 @@
     androidTestImplementation(libs.testRules)
 }
 
+androidx {
+    name = "Dummy Project for test runner"
+}
+
 android {
-    namespace = "androidx.testinfra.placeholderintegrationtest"
+    namespace "androidx.testinfra.placeholderintegrationtest"
 }
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index 3ad3638..c46795c 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -31,7 +31,8 @@
 # Disabled due to https://github.com/gradle/gradle/issues/18626
 # org.gradle.vfs.watch=true
 org.gradle.dependency.verification.console=verbose
-org.gradle.configuration-cache=true
+# Disable config cache due to b/325886853. Should revert to true after bug is fixed.
+org.gradle.configuration-cache=false
 org.gradle.configuration-cache.problems=fail
 
 android.lint.printStackTrace=true
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
index 4900663..70e098e 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -169,6 +169,11 @@
 
     public void enableVerboseLogging() {
         runShellCommand("setprop log.tag.adservices VERBOSE");
+        runShellCommand("setprop log.tag.adservices.adid VERBOSE");
+        runShellCommand("setprop log.tag.adservices.appsetid VERBOSE");
+        runShellCommand("setprop log.tag.adservices.topics VERBOSE");
+        runShellCommand("setprop log.tag.adservices.fledge VERBOSE");
+        runShellCommand("setprop log.tag.adservices.measurement VERBOSE");
     }
 
     public void overrideFledgeSelectAdsKillSwitch(boolean override) {
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
index fb2fa2f..19cec1b 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/appsetid/AppSetIdManagerTest.java
@@ -33,6 +33,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.concurrent.TimeUnit;
+
 @RunWith(JUnit4.class)
 @SdkSuppress(minSdkVersion = 28) // API 28 required for device_config used by this test
 // TODO: Consider refactoring so that we're not duplicating code.
@@ -45,10 +47,11 @@
         mTestUtil.overrideAppSetIdKillSwitch(true);
         mTestUtil.overrideKillSwitches(true);
         mTestUtil.overrideAllowlists(true);
+        mTestUtil.enableVerboseLogging();
 
         // Put in a short sleep to make sure the updated config propagates
         // before starting the tests
-        Thread.sleep(100);
+        TimeUnit.SECONDS.sleep(1);
     }
 
     @After
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 0d5c66b..3c223df 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -1874,6 +1874,46 @@
                 assertWithMessage("$qName  enum entries are not type elements")
                     .that(typeElement.getEnclosedElements().filter { it.isTypeElement() })
                     .isEmpty()
+                // TODO(kuanyingchou): https://github.com/google/ksp/issues/1761
+                val parent = typeElement.superClass!!.typeElement!!
+                if (qName == "test.KotlinEnum" && !isPreCompiled && invocation.isKsp) {
+                    assertThat(parent.asClassName()).isEqualTo(Any::class.asClassName())
+                } else {
+                    assertThat(parent.asClassName()).isEqualTo(Enum::class.asClassName())
+                }
+                // TODO(kuanyingchou): make this more consistent.
+                val methodNames = typeElement.getDeclaredMethods().map { it.jvmName }
+                if (qName == "test.KotlinEnum") {
+                    if (invocation.isKsp) {
+                        if (isPreCompiled) {
+                            assertThat(methodNames).containsExactly(
+                                "enumMethod",
+                                "values",
+                                "valueOf",
+                            )
+                        } else {
+                            // `values` and `valueOf` will be added in KSP2.
+                            assertThat(methodNames).containsExactly(
+                                "enumMethod",
+                            )
+                        }
+                    } else {
+                        assertThat(methodNames).containsExactly(
+                            "values",
+                            "valueOf",
+                            "enumMethod",
+                            // `entries` became stable in Kotlin 1.9.0 but somehow only appears
+                            // in KAPT. We can't find an `entries` property in KSP yet.
+                            "getEntries"
+                        )
+                    }
+                } else {
+                    assertThat(methodNames).containsExactly(
+                        "values",
+                        "valueOf",
+                        "enumMethod",
+                    )
+                }
             }
         }
     }
diff --git a/slice/slice-view/api/removed_current.txt b/slice/slice-view/api/removed_current.txt
deleted file mode 100644
index 62b0eea..0000000
--- a/slice/slice-view/api/removed_current.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-// Signature format: 4.0
-package androidx.slice.widget {
-
-  @Deprecated public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
-    method @Deprecated public void showActionDividers(boolean);
-    method @Deprecated public void showHeaderDivider(boolean);
-    method @Deprecated public void showTitleItems(boolean);
-  }
-
-}
-
diff --git a/slice/slice-view/api/restricted_current.txt b/slice/slice-view/api/restricted_current.txt
index 9a855f0..0864f0a 100644
--- a/slice/slice-view/api/restricted_current.txt
+++ b/slice/slice-view/api/restricted_current.txt
@@ -275,6 +275,9 @@
     method @Deprecated public void setShowTitleItems(boolean);
     method @Deprecated public void setSlice(androidx.slice.Slice?);
     method @Deprecated public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void showActionDividers(boolean);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void showHeaderDivider(boolean);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void showTitleItems(boolean);
     field @Deprecated public static final int MODE_LARGE = 2; // 0x2
     field @Deprecated public static final int MODE_SHORTCUT = 3; // 0x3
     field @Deprecated public static final int MODE_SMALL = 1; // 0x1
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
index cec0e521..9d0881a 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
@@ -21,6 +21,7 @@
 import static android.view.View.MeasureSpec.EXACTLY;
 import static android.view.View.MeasureSpec.UNSPECIFIED;
 import static android.view.View.MeasureSpec.makeMeasureSpec;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
 import android.app.PendingIntent;
 import android.content.Context;
@@ -673,9 +674,9 @@
 
     /**
      * @deprecated TO BE REMOVED
-     * @removed
      */
     @Deprecated
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
     public void showTitleItems(boolean enabled) {
         setShowTitleItems(enabled);
     }
@@ -692,9 +693,9 @@
 
     /**
      * @deprecated TO BE REMOVED
-     * @removed
      */
     @Deprecated
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
     public void showHeaderDivider(boolean enabled) {
         setShowHeaderDivider(enabled);
     }
@@ -711,9 +712,9 @@
 
     /**
      * @deprecated TO BE REMOVED
-     * @removed
      */
     @Deprecated
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
     public void showActionDividers(boolean enabled) {
         setShowActionDividers(enabled);
     }
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/PositionIndicatorBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/PositionIndicatorBenchmark.kt
index bae65ab..e99e4b21 100644
--- a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/PositionIndicatorBenchmark.kt
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/PositionIndicatorBenchmark.kt
@@ -17,6 +17,10 @@
 package androidx.wear.compose.material.benchmark
 
 import androidx.compose.animation.core.snap
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableFloatStateOf
@@ -27,9 +31,13 @@
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
 import androidx.compose.testutils.setupContent
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.PositionIndicator
 import androidx.wear.compose.material.PositionIndicatorDefaults
@@ -42,17 +50,17 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-class PositionIndicatorBenchmark {
+class SimplePositionIndicatorBenchmark {
     @get:Rule
     val benchmarkRule = ComposeBenchmarkRule()
     private val defaultPositionIndicatorCaseFactory = {
-        PositionIndicatorBenchmarkTestCase(animate = false)
+        SimplePositionIndicatorBenchmarkTestCase(animate = false)
     }
 
     @Test
     fun changeFraction() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 animate = false
             )
@@ -62,7 +70,7 @@
     @Test
     fun changeFractionAndSizeFraction_hide() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 targetSizeFraction = 0.5f,
                 targetVisibility = PositionIndicatorVisibility.Hide,
@@ -74,7 +82,7 @@
     @Test
     fun changeFractionAndSizeFraction_autoHide() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 targetSizeFraction = 0.5f,
                 targetVisibility = PositionIndicatorVisibility.AutoHide,
@@ -86,7 +94,7 @@
     @Test
     fun changeFraction_withAnimation() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 animate = true
             )
@@ -96,7 +104,7 @@
     @Test
     fun changeFractionAndSizeFraction_hide_withAnimation() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 targetSizeFraction = 0.5f,
                 targetVisibility = PositionIndicatorVisibility.Hide,
@@ -108,7 +116,7 @@
     @Test
     fun changeFractionAndSizeFraction_autoHide_withAnimation() {
         benchmarkRule.changePositionBenchmark {
-            PositionIndicatorBenchmarkTestCase(
+            SimplePositionIndicatorBenchmarkTestCase(
                 targetFraction = 0.5f,
                 targetSizeFraction = 0.5f,
                 targetVisibility = PositionIndicatorVisibility.AutoHide,
@@ -123,17 +131,38 @@
     }
 }
 
-internal class PositionIndicatorBenchmarkTestCase(
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class PositionIndicatorWithScalingLazyColumnBenchmark {
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    @Test
+    fun positionIndicator_withScalingLazyColumn_withScroll() {
+        benchmarkRule.changePositionBenchmark {
+            PositionIndicatorWithScalingLazyColumnBenchmarkTestCase(true)
+        }
+    }
+
+    @Test
+    fun positionIndicator_withScalingLazyColumn_noScroll() {
+        benchmarkRule.changePositionBenchmark {
+            PositionIndicatorWithScalingLazyColumnBenchmarkTestCase(false)
+        }
+    }
+}
+
+internal class SimplePositionIndicatorBenchmarkTestCase(
     val targetFraction: Float? = null,
     val targetSizeFraction: Float? = null,
     val targetVisibility: PositionIndicatorVisibility? = null,
     val animate: Boolean
-) : LayeredComposeTestCase() {
+) : PositionIndicatorBenchmarkTestCase() {
     private lateinit var positionFraction: MutableState<Float>
     private lateinit var sizeFraction: MutableState<Float>
     private lateinit var visibility: MutableState<PositionIndicatorVisibility>
 
-    fun onChange() {
+    override fun onChange() {
         runBlocking {
             targetFraction?.let { positionFraction.value = targetFraction }
             targetSizeFraction?.let { sizeFraction.value = targetSizeFraction }
@@ -182,6 +211,56 @@
     }
 }
 
+internal class PositionIndicatorWithScalingLazyColumnBenchmarkTestCase(
+    private val withScroll: Boolean = false
+) : PositionIndicatorBenchmarkTestCase() {
+    private lateinit var slcState: ScalingLazyListState
+
+    override fun onChange() {
+        runBlocking {
+            if (withScroll) slcState.scrollToItem(30)
+        }
+    }
+
+    @Composable
+    override fun MeasuredContent() {
+        slcState = rememberScalingLazyListState()
+        Box(modifier = Modifier.fillMaxSize()) {
+            ScalingLazyColumn(
+                state = slcState,
+            ) {
+                items(50) {
+                    // By changing the size we can also change the size of the PositionIndicator,
+                    // which will allow us to better measure all parts of PositionIndicator math.
+                    val height = it.dp
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .height(height)
+                    )
+                }
+            }
+            PositionIndicator(
+                scalingLazyListState = slcState,
+                fadeInAnimationSpec = snap(),
+                fadeOutAnimationSpec = snap(),
+                positionAnimationSpec = snap()
+            )
+        }
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme {
+            content()
+        }
+    }
+}
+
+internal abstract class PositionIndicatorBenchmarkTestCase : LayeredComposeTestCase() {
+    abstract fun onChange()
+}
+
 private class CustomPositionIndicatorState(
     private val _positionFraction: () -> Float,
     private val sizeFraction: () -> Float,
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Ripple.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Ripple.kt
index 06fd1c6..2c3da14 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Ripple.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Ripple.kt
@@ -243,28 +243,29 @@
 }
 
 private class DelegatingThemeAwareRippleNode(
-    private val interactionSource: InteractionSource,
-    private val bounded: Boolean,
-    private val radius: Dp,
-    private val color: ColorProducer,
+    interactionSource: InteractionSource,
+    bounded: Boolean,
+    radius: Dp,
+    color: ColorProducer,
 ) : DelegatingNode(), CompositionLocalConsumerModifierNode {
-    override fun onAttach() {
-        val calculateColor = ColorProducer {
-            val userDefinedColor = color()
-            if (userDefinedColor.isSpecified) {
-                userDefinedColor
-            } else {
-                RippleDefaults.rippleColor(contentColor = currentValueOf(LocalContentColor))
-            }
-        }
 
-        delegate(createRippleModifierNode(
-            interactionSource,
-            bounded,
-            radius,
-            calculateColor,
-            CalculateRippleAlpha
-        ))
+    init {
+        delegate(
+            createRippleModifierNode(
+                interactionSource,
+                bounded,
+                radius,
+                ColorProducer {
+                    val userDefinedColor = color()
+                    if (userDefinedColor.isSpecified) {
+                        userDefinedColor
+                    } else {
+                        RippleDefaults.rippleColor(currentValueOf(LocalContentColor))
+                    }
+                },
+                CalculateRippleAlpha
+            )
+        )
     }
 }
 
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
index 02209a1..5155500 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
@@ -215,28 +215,29 @@
 }
 
 private class DelegatingThemeAwareRippleNode(
-    private val interactionSource: InteractionSource,
-    private val bounded: Boolean,
-    private val radius: Dp,
-    private val color: ColorProducer,
+    interactionSource: InteractionSource,
+    bounded: Boolean,
+    radius: Dp,
+    color: ColorProducer,
 ) : DelegatingNode(), CompositionLocalConsumerModifierNode {
-    override fun onAttach() {
-        val calculateColor = ColorProducer {
-            val userDefinedColor = color()
-            if (userDefinedColor.isSpecified) {
-                userDefinedColor
-            } else {
-                currentValueOf(LocalContentColor)
-            }
-        }
 
-        delegate(createRippleModifierNode(
-            interactionSource,
-            bounded,
-            radius,
-            calculateColor,
-            CalculateRippleAlpha
-        ))
+    init {
+        delegate(
+            createRippleModifierNode(
+                interactionSource,
+                bounded,
+                radius,
+                ColorProducer {
+                    val userDefinedColor = color()
+                    if (userDefinedColor.isSpecified) {
+                        userDefinedColor
+                    } else {
+                        currentValueOf(LocalContentColor)
+                    }
+                },
+                CalculateRippleAlpha
+            )
+        )
     }
 }
 
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 5e49966..8e73af5 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -112,7 +112,15 @@
 public class DynamicTypeEvaluator {
     private static final String TAG = "DynamicTypeEvaluator";
     private static final QuotaManager NO_OP_QUOTA_MANAGER =
-            new FixedQuotaManagerImpl(Integer.MAX_VALUE, "dynamic nodes noop");
+            new QuotaManager() {
+                @Override
+                public boolean tryAcquireQuota(int quota) {
+                    return true;
+                }
+                @Override
+                public void releaseQuota(int quota) {
+                }
+            };
 
     @NonNull
     private static final QuotaManager DISABLED_ANIMATIONS_QUOTA_MANAGER =
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 9e83109..5f080c8 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -5,5 +5,5 @@
     "preinstalled on WearOS devices."
 gerrit source: "vendor/google_clockwork/sdk/lib"
 API version: 34.1
-Build ID: 11091675
-Last updated: Tue Nov 14 01:35:30 AM UTC 2023
+Build ID: 11505490
+Last updated: Wed Feb 28 02:47:27 AM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 4ba1293..16fe240 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ