Merge "Upgrade to AGP 8.3.0-beta01" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index d977458..322f5ec 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -19,7 +19,6 @@
 import android.app.Activity
 import android.content.ActivityNotFoundException
 import android.content.Intent
-import android.os.Build
 import android.os.Bundle
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.ActivityResultLauncher
@@ -69,12 +68,6 @@
 
     @Test
     fun registerBeforeOnCreateTest() {
-        // There is a leak in API 30 InputMethodManager that causes this test to be flaky.
-        // Once https://github.com/square/leakcanary/issues/2592 is addressed we can upgrade
-        // leak canary and remove this.
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
-            return
-        }
         ActivityScenario.launch(RegisterBeforeOnCreateActivity::class.java).use { scenario ->
             scenario.withActivity {
                 recreate()
@@ -92,12 +85,6 @@
 
     @Test
     fun registerInInitTest() {
-        // There is a leak in API 30 InputMethodManager that causes this test to be flaky.
-        // Once https://github.com/square/leakcanary/issues/2592 is addressed we can upgrade
-        // leak canary and remove this.
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
-            return
-        }
         ActivityScenario.launch(RegisterInInitActivity::class.java).use { scenario ->
             scenario.withActivity {
                 recreate()
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
index 903b129..98e187e 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.os.Build
+import androidx.benchmark.Arguments
 import androidx.benchmark.Outputs
 import androidx.benchmark.Shell
 import androidx.benchmark.macro.junit4.BaselineProfileRule
@@ -60,7 +61,7 @@
 
         // Collects the baseline profile
         baselineRule.collect(
-            packageName = PACKAGE_NAME,
+            packageName = Arguments.getTargetPackageNameOrThrow(),
             filterPredicate = { it.contains(PROFILE_LINE_EMPTY_ACTIVITY) },
             maxIterations = 1,
             profileBlock = {
@@ -88,7 +89,7 @@
 
         // Collects the baseline profile
         baselineRule.collect(
-            packageName = PACKAGE_NAME,
+            packageName = Arguments.getTargetPackageNameOrThrow(),
             filterPredicate = { it.contains(PROFILE_LINE_EMPTY_ACTIVITY) },
             includeInStartupProfile = true,
             maxIterations = 1,
@@ -114,8 +115,6 @@
             "androidx.benchmark.integration.macrobenchmark.target.EMPTY_ACTIVITY"
         private const val PROFILE_LINE_EMPTY_ACTIVITY =
             "androidx/benchmark/integration/macrobenchmark/target/EmptyActivity;"
-        private const val PACKAGE_NAME =
-            "androidx.benchmark.integration.macrobenchmark.target"
     }
 
     private fun List<String>.assertContainsInOrder(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index 17d8170..3c98b3e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -48,7 +48,6 @@
     val allArgs =
         args +
             listOf(
-                "--update-kotlin-nulls", // b/309149849: temporary feature flag
                 "--hide",
                 "HiddenSuperclass", // We allow having a hidden parent class
                 "--hide",
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index 5a4f191..c088ac2 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -38,22 +38,17 @@
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
-import androidx.camera.core.CameraEffect
-import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.CameraInfo
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.DynamicRange
 import androidx.camera.core.impl.utils.TransformUtils.rotateSize
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.processing.DefaultSurfaceProcessor
-import androidx.camera.core.processing.SurfaceProcessorInternal
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.impl.CameraPipeConfigTestRule
 import androidx.camera.testing.impl.CameraUtil
 import androidx.camera.testing.impl.WakelockEmptyActivityRule
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
-import androidx.camera.testing.impl.fakes.FakeSurfaceEffect
 import androidx.core.util.Consumer
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
@@ -142,7 +137,6 @@
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context: Context = ApplicationProvider.getApplicationContext()
-    private val surfaceProcessorsToRelease = mutableListOf<SurfaceProcessorInternal>()
     // TODO(b/278168212): Only SDR is checked by now. Need to extend to HDR dynamic ranges.
     private val dynamicRange = DynamicRange.SDR
     private lateinit var cameraProvider: ProcessCameraProvider
@@ -185,10 +179,6 @@
         if (this::cameraProvider.isInitialized) {
             cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
         }
-        for (surfaceProcessor in surfaceProcessorsToRelease) {
-            surfaceProcessor.release()
-        }
-        surfaceProcessorsToRelease.clear()
     }
 
     @Test
@@ -197,20 +187,23 @@
     }
 
     @Test
-    fun qualityOptionCanRecordVideo_enableSurfaceProcessor() {
+    fun qualityOptionCanRecordVideo_enableSurfaceProcessing() {
         assumeSuccessfulSurfaceProcessing()
 
-        testQualityOptionRecordVideo(effect = createEffect())
+        testQualityOptionRecordVideo(enableSurfaceProcessing = true)
     }
 
-    private fun testQualityOptionRecordVideo(effect: CameraEffect? = null) {
+    private fun testQualityOptionRecordVideo(enableSurfaceProcessing: Boolean = false) {
         // Arrange.
         val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
         val videoProfile =
             videoCapabilities.getProfiles(quality, dynamicRange)!!.defaultVideoProfile
         val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(quality)).build()
-        val videoCapture = VideoCapture.withOutput(recorder)
-        videoCapture.effect = effect
+        val videoCapture = VideoCapture.Builder(recorder).apply {
+            if (enableSurfaceProcessing) {
+                setSurfaceProcessingForceEnabled()
+            }
+        }.build()
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
         val latchForRecordingStatus = CountDownLatch(5)
         val latchForRecordingFinalized = CountDownLatch(1)
@@ -241,6 +234,11 @@
             )
         }
 
+        if (enableSurfaceProcessing) {
+            // Ensure the surface processing is enabled.
+            assertThat(isSurfaceProcessingEnabled(videoCapture)).isTrue()
+        }
+
         // Act.
         videoCapture.startVideoRecording(file, eventListener).use {
             // Verify the recording proceed for a while.
@@ -267,15 +265,6 @@
         file.delete()
     }
 
-    private fun createEffect(): CameraEffect {
-        val fakeSurfaceProcessor = DefaultSurfaceProcessor.Factory.newInstance(DynamicRange.SDR)
-        surfaceProcessorsToRelease.add(fakeSurfaceProcessor)
-        return FakeSurfaceEffect(
-            VIDEO_CAPTURE,
-            fakeSurfaceProcessor
-        )
-    }
-
     private fun VideoCapture<Recorder>.startVideoRecording(
         file: File,
         eventListener: Consumer<VideoRecordEvent>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 27c9214..c514c78 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -40,7 +40,6 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.Preview
-import androidx.camera.core.UseCase
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
@@ -1230,11 +1229,6 @@
         assumeExtraCroppingQuirk(implName)
     }
 
-    private fun isStreamSharingEnabled(useCase: UseCase) = !useCase.camera!!.hasTransform
-
-    private fun isSurfaceProcessingEnabled(videoCapture: VideoCapture<*>) =
-        videoCapture.node != null || isStreamSharingEnabled(videoCapture)
-
     private class ImageSavedCallback :
         ImageCapture.OnImageSavedCallback {
 
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
index 61b85f5..8e220bd 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
@@ -28,6 +28,7 @@
 import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks as PipeDeviceQuirks
 import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk as PipeExtraCroppingQuirk
 import androidx.camera.core.CameraInfo
+import androidx.camera.core.UseCase
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
 import androidx.camera.video.internal.compat.quirk.StopCodecAfterSurfaceRemovalCrashMediaServerQuirk
 import com.google.common.truth.Truth.assertThat
@@ -82,3 +83,10 @@
         assertThat(it.getRotatedResolution()).isEqualTo(expectedResolution)
     }
 }
+
+@RequiresApi(21)
+fun isStreamSharingEnabled(useCase: UseCase) = !useCase.camera!!.hasTransform
+
+@RequiresApi(21)
+fun isSurfaceProcessingEnabled(videoCapture: VideoCapture<*>) =
+    videoCapture.node != null || isStreamSharingEnabled(videoCapture)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 372c681..b31f1f6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -44,6 +44,7 @@
 import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.video.QualitySelector.getQualityToResolutionMap;
 import static androidx.camera.video.StreamInfo.STREAM_ID_ERROR;
+import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_FORCE_ENABLE_SURFACE_PROCESSING;
 import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_ENCODER_INFO_FINDER;
 import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_OUTPUT;
 import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoEncoderConfig;
@@ -620,7 +621,7 @@
             // pipeline when the transformation becomes invalid.
             mHasCompensatingTransformation = true;
         }
-        mNode = createNodeIfNeeded(camera, mCropRect, resolution, dynamicRange);
+        mNode = createNodeIfNeeded(camera, config, mCropRect, resolution, dynamicRange);
         // Choose Timebase based on the whether the buffer is copied.
         Timebase timebase;
         if (mNode != null || !camera.getHasTransform()) {
@@ -901,10 +902,12 @@
 
     @Nullable
     private SurfaceProcessorNode createNodeIfNeeded(@NonNull CameraInternal camera,
+            @NonNull VideoCaptureConfig<T> config,
             @NonNull Rect cropRect,
             @NonNull Size resolution,
             @NonNull DynamicRange dynamicRange) {
         if (getEffect() != null
+                || shouldEnableSurfaceProcessingByConfig(camera, config)
                 || shouldEnableSurfaceProcessingByQuirk(camera)
                 || shouldCrop(cropRect, resolution)
                 || shouldMirror(camera)
@@ -1092,6 +1095,13 @@
                 || resolution.getHeight() != cropRect.height();
     }
 
+    private static <T extends VideoOutput> boolean shouldEnableSurfaceProcessingByConfig(
+            @NonNull CameraInternal camera, @NonNull VideoCaptureConfig<T> config) {
+        // If there has been a buffer copy, it means the surface processing is already enabled on
+        // input stream. Otherwise, enable it as needed.
+        return camera.getHasTransform() && config.isSurfaceProcessingForceEnabled();
+    }
+
     private static boolean shouldEnableSurfaceProcessingByQuirk(@NonNull CameraInternal camera) {
         // If there has been a buffer copy, it means the surface processing is already enabled on
         // input stream. Otherwise, enable it as needed.
@@ -1874,5 +1884,26 @@
             getMutableConfig().insertOption(OPTION_CAPTURE_TYPE, captureType);
             return this;
         }
+
+        /**
+         * Forces surface processing to be enabled.
+         *
+         * <p>Typically, surface processing is automatically enabled only when required for a
+         * specific effect. However, calling this method will force it to be enabled even if no
+         * effect is required. Surface processing creates additional processing through the OpenGL
+         * pipeline, affecting performance and memory usage. Camera service may treat the surface
+         * differently, potentially impacting video quality and stabilization. So it is generally
+         * not recommended to enable it.
+         *
+         * <p>One example where it might be useful is to work around device compatibility issues.
+         * For example, UHD video recording might not work on some devices, but enabling surface
+         * processing could work around the issue.
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder<T> setSurfaceProcessingForceEnabled() {
+            getMutableConfig().insertOption(OPTION_FORCE_ENABLE_SURFACE_PROCESSING, true);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
index c9bd4de..4cc1477 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
@@ -16,6 +16,10 @@
 
 package androidx.camera.video.impl;
 
+import static androidx.core.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.arch.core.util.Function;
@@ -30,8 +34,6 @@
 import androidx.camera.video.internal.encoder.VideoEncoderConfig;
 import androidx.camera.video.internal.encoder.VideoEncoderInfo;
 
-import java.util.Objects;
-
 /**
  * Config for a video capture use case.
  *
@@ -55,23 +57,31 @@
             OPTION_VIDEO_ENCODER_INFO_FINDER =
             Option.create("camerax.video.VideoCapture.videoEncoderInfoFinder", Function.class);
 
+    public static final Option<Boolean> OPTION_FORCE_ENABLE_SURFACE_PROCESSING = Option.create(
+            "camerax.video.VideoCapture.forceEnableSurfaceProcessing", Boolean.class);
+
     // *********************************************************************************************
 
     private final OptionsBundle mConfig;
 
     public VideoCaptureConfig(@NonNull OptionsBundle config) {
+        checkArgument(config.containsOption(OPTION_VIDEO_OUTPUT));
         mConfig = config;
     }
 
     @SuppressWarnings("unchecked")
     @NonNull
     public T getVideoOutput() {
-        return (T) retrieveOption(OPTION_VIDEO_OUTPUT);
+        return (T) requireNonNull(retrieveOption(OPTION_VIDEO_OUTPUT));
     }
 
     @NonNull
     public Function<VideoEncoderConfig, VideoEncoderInfo> getVideoEncoderInfoFinder() {
-        return Objects.requireNonNull(retrieveOption(OPTION_VIDEO_ENCODER_INFO_FINDER));
+        return requireNonNull(retrieveOption(OPTION_VIDEO_ENCODER_INFO_FINDER));
+    }
+
+    public boolean isSurfaceProcessingForceEnabled() {
+        return requireNonNull(retrieveOption(OPTION_FORCE_ENABLE_SURFACE_PROCESSING, false));
     }
 
     /**
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index a9eb715..387bef3 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -94,6 +94,70 @@
     property public final int last;
   }
 
+  public abstract sealed class DoubleList {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final operator boolean contains(double element);
+    method public final boolean containsAll(androidx.collection.DoubleList elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final double elementAt(@IntRange(from=0L) int index);
+    method public final inline double elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Double> defaultValue);
+    method public final double first();
+    method public final inline double first(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super java.lang.Double,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super java.lang.Double,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super java.lang.Double,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Double,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Double,kotlin.Unit> block);
+    method public final operator double get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(double element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final double last();
+    method public final inline double last(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(double element);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+  }
+
+  public final class DoubleListKt {
+    method public static androidx.collection.DoubleList doubleListOf();
+    method public static androidx.collection.DoubleList doubleListOf(double element1);
+    method public static androidx.collection.DoubleList doubleListOf(double element1, double element2);
+    method public static androidx.collection.DoubleList doubleListOf(double element1, double element2, double element3);
+    method public static androidx.collection.DoubleList doubleListOf(double... elements);
+    method public static androidx.collection.DoubleList emptyDoubleList();
+    method public static inline androidx.collection.MutableDoubleList mutableDoubleListOf();
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1);
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1, double element2);
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1, double element2, double element3);
+    method public static inline androidx.collection.MutableDoubleList mutableDoubleListOf(double... elements);
+  }
+
   public abstract sealed class FloatFloatMap {
     method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -1142,6 +1206,37 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V?> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V?,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableDoubleList extends androidx.collection.DoubleList {
+    ctor public MutableDoubleList(optional int initialCapacity);
+    method public boolean add(double element);
+    method public void add(@IntRange(from=0L) int index, double element);
+    method public boolean addAll(androidx.collection.DoubleList elements);
+    method public boolean addAll(double[] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.DoubleList elements);
+    method public boolean addAll(@IntRange(from=0L) int index, double[] elements);
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.DoubleList elements);
+    method public inline operator void minusAssign(double element);
+    method public operator void minusAssign(double[] elements);
+    method public operator void plusAssign(androidx.collection.DoubleList elements);
+    method public inline operator void plusAssign(double element);
+    method public operator void plusAssign(double[] elements);
+    method public boolean remove(double element);
+    method public boolean removeAll(androidx.collection.DoubleList elements);
+    method public boolean removeAll(double[] elements);
+    method public double removeAt(@IntRange(from=0L) int index);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.DoubleList elements);
+    method public boolean retainAll(double[] elements);
+    method public operator double set(@IntRange(from=0L) int index, double element);
+    method public void sort();
+    method public void sortDescending();
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
   public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
     ctor public MutableFloatFloatMap(optional int initialCapacity);
     method public void clear();
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index 91368d2..8b4f85d 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -94,6 +94,72 @@
     property public final int last;
   }
 
+  public abstract sealed class DoubleList {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final operator boolean contains(double element);
+    method public final boolean containsAll(androidx.collection.DoubleList elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final double elementAt(@IntRange(from=0L) int index);
+    method public final inline double elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Double> defaultValue);
+    method public final double first();
+    method public final inline double first(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super java.lang.Double,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super java.lang.Double,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super java.lang.Double,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Double,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super java.lang.Double,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Double,kotlin.Unit> block);
+    method public final operator double get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(double element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final String joinToString();
+    method public final String joinToString(optional CharSequence separator);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit);
+    method public final String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, optional CharSequence truncated, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, optional int limit, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, optional CharSequence postfix, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, optional CharSequence prefix, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(optional CharSequence separator, kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final inline String joinToString(kotlin.jvm.functions.Function1<? super java.lang.Double,? extends java.lang.CharSequence> transform);
+    method public final double last();
+    method public final inline double last(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(double element);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+    field @kotlin.PublishedApi internal int _size;
+    field @kotlin.PublishedApi internal double[] content;
+  }
+
+  public final class DoubleListKt {
+    method public static androidx.collection.DoubleList doubleListOf();
+    method public static androidx.collection.DoubleList doubleListOf(double element1);
+    method public static androidx.collection.DoubleList doubleListOf(double element1, double element2);
+    method public static androidx.collection.DoubleList doubleListOf(double element1, double element2, double element3);
+    method public static androidx.collection.DoubleList doubleListOf(double... elements);
+    method public static androidx.collection.DoubleList emptyDoubleList();
+    method public static inline androidx.collection.MutableDoubleList mutableDoubleListOf();
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1);
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1, double element2);
+    method public static androidx.collection.MutableDoubleList mutableDoubleListOf(double element1, double element2, double element3);
+    method public static inline androidx.collection.MutableDoubleList mutableDoubleListOf(double... elements);
+  }
+
   public abstract sealed class FloatFloatMap {
     method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -1214,6 +1280,37 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V?> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V?,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableDoubleList extends androidx.collection.DoubleList {
+    ctor public MutableDoubleList(optional int initialCapacity);
+    method public boolean add(double element);
+    method public void add(@IntRange(from=0L) int index, double element);
+    method public boolean addAll(androidx.collection.DoubleList elements);
+    method public boolean addAll(double[] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.DoubleList elements);
+    method public boolean addAll(@IntRange(from=0L) int index, double[] elements);
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.DoubleList elements);
+    method public inline operator void minusAssign(double element);
+    method public operator void minusAssign(double[] elements);
+    method public operator void plusAssign(androidx.collection.DoubleList elements);
+    method public inline operator void plusAssign(double element);
+    method public operator void plusAssign(double[] elements);
+    method public boolean remove(double element);
+    method public boolean removeAll(androidx.collection.DoubleList elements);
+    method public boolean removeAll(double[] elements);
+    method public double removeAt(@IntRange(from=0L) int index);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.DoubleList elements);
+    method public boolean retainAll(double[] elements);
+    method public operator double set(@IntRange(from=0L) int index, double element);
+    method public void sort();
+    method public void sortDescending();
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
   public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
     ctor public MutableFloatFloatMap(optional int initialCapacity);
     method public void clear();
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
new file mode 100644
index 0000000..9ba38be
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
@@ -0,0 +1,972 @@
+/*
+ * Copyright 2023 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+/**
+ * [DoubleList] is a [List]-like collection for [Double] values. It allows retrieving
+ * the elements without boxing. [DoubleList] is always backed by a [MutableDoubleList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+public sealed class DoubleList(initialCapacity: Int) {
+    @JvmField
+    @PublishedApi
+    internal var content: DoubleArray = if (initialCapacity == 0) {
+        EmptyDoubleArray
+    } else {
+        DoubleArray(initialCapacity)
+    }
+
+    @Suppress("PropertyName")
+    @JvmField
+    @PublishedApi
+    internal var _size: Int = 0
+
+    /**
+     * The number of elements in the [DoubleList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns the last valid index in the [DoubleList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = _size - 1
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [DoubleList].
+     */
+    public inline val indices: IntRange get() = 0 until _size
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public fun none(): Boolean {
+        return isEmpty()
+    }
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public fun any(): Boolean {
+        return isNotEmpty()
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: Double) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: Double) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEachReversed {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [DoubleList] contains [element] or `false` otherwise.
+     */
+    public operator fun contains(element: Double): Boolean {
+        forEach {
+            if (it == element) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [DoubleList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: DoubleList): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public fun count(): Int = _size
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: Double) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { if (predicate(it)) count++ }
+        return count
+    }
+
+    /**
+     * Returns the first element in the [DoubleList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun first(): Double {
+        if (isEmpty()) {
+            throw NoSuchElementException("DoubleList is empty.")
+        }
+        return content[0]
+    }
+
+    /**
+     * Returns the first element in the [DoubleList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: Double) -> Boolean): Double {
+        contract { callsInPlace(predicate) }
+        forEach { item ->
+            if (predicate(item)) return item
+        }
+        throw NoSuchElementException("DoubleList contains no element matching the predicate.")
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [DoubleList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: Double) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEach { item ->
+            acc = operation(acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [DoubleList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: Double) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachIndexed { i, item ->
+            acc = operation(i, acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [DoubleList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: Double, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversed { item ->
+            acc = operation(item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [DoubleList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: Double, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversedIndexed { i, item ->
+            acc = operation(i, item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Calls [block] for each element in the [DoubleList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: Double) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [DoubleList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: Double) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [DoubleList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: Double) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [DoubleList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: Double) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): Double {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): Double {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> Double
+    ): Double {
+        if (index !in 0 until _size) {
+            return defaultValue(index)
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the index of [element] in the [DoubleList] or `-1` if [element] is not there.
+     */
+    public fun indexOf(element: Double): Int {
+        forEachIndexed { i, item ->
+            if (element == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the first element in the [DoubleList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: Double) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the last element in the [DoubleList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: Double) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachReversedIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns `true` if the [DoubleList] has no elements in it or `false` otherwise.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if there are elements in the [DoubleList] or `false` if it is empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the last element in the [DoubleList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun last(): Double {
+        if (isEmpty()) {
+            throw NoSuchElementException("DoubleList is empty.")
+        }
+        return content[lastIndex]
+    }
+
+    /**
+     * Returns the last element in the [DoubleList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: Double) -> Boolean): Double {
+        contract { callsInPlace(predicate) }
+        forEachReversed { item ->
+            if (predicate(item)) {
+                return item
+            }
+        }
+        throw NoSuchElementException("DoubleList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the index of the last element in the [DoubleList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public fun lastIndexOf(element: Double): Int {
+        forEachReversedIndexed { i, item ->
+            if (item == element) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+    ): String = buildString {
+        append(prefix)
+        this@DoubleList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(element)
+        }
+        append(postfix)
+    }
+
+    /**
+     * Creates a String from the elements separated by [separator] and using [prefix] before
+     * and [postfix] after, if supplied. [transform] dictates how each element will be represented.
+     *
+     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used
+     * to generate the string. If the collection holds more than [limit] items, the string
+     * is terminated with [truncated].
+     */
+    @JvmOverloads
+    public inline fun joinToString(
+        separator: CharSequence = ", ",
+        prefix: CharSequence = "",
+        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
+        limit: Int = -1,
+        truncated: CharSequence = "...",
+        crossinline transform: (Double) -> CharSequence
+    ): String = buildString {
+        append(prefix)
+        this@DoubleList.forEachIndexed { index, element ->
+            if (index == limit) {
+                append(truncated)
+                return@buildString
+            }
+            if (index != 0) {
+                append(separator)
+            }
+            append(transform(element))
+        }
+        append(postfix)
+    }
+
+    /**
+     * Returns a hash code based on the contents of the [DoubleList].
+     */
+    override fun hashCode(): Int {
+        var hashCode = 0
+        forEach { element ->
+            hashCode += 31 * element.hashCode()
+        }
+        return hashCode
+    }
+
+    /**
+     * Returns `true` if [other] is a [DoubleList] and the contents of this and [other] are the
+     * same.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (other !is DoubleList || other._size != _size) {
+            return false
+        }
+        val content = content
+        val otherContent = other.content
+        for (i in indices) {
+            if (content[i] != otherContent[i]) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String = joinToString(prefix = "[", postfix = "]")
+}
+
+/**
+ * [MutableDoubleList] is a [MutableList]-like collection for [Double] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [DoubleList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutableDoubleList] with a [capacity] of `initialCapacity`.
+ */
+public class MutableDoubleList(
+    initialCapacity: Int = 16
+) : DoubleList(initialCapacity) {
+    /**
+     * Returns the total number of elements that can be held before the [MutableDoubleList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = content.size
+
+    /**
+     * Adds [element] to the [MutableDoubleList] and returns `true`.
+     */
+    public fun add(element: Double): Boolean {
+        ensureCapacity(_size + 1)
+        content[_size] = element
+        _size++
+        return true
+    }
+
+    /**
+     * Adds [element] to the [MutableDoubleList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: Double) {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        ensureCapacity(_size + 1)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        content[index] = element
+        _size++
+    }
+
+    /**
+     * Adds all [elements] to the [MutableDoubleList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableDoubleList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: DoubleArray
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.copyInto(content, index)
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutableDoubleList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableDoubleList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: DoubleList
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements._size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements._size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = index,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableDoubleList] and returns `true` if the
+     * [MutableDoubleList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: DoubleList): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableDoubleList] and returns `true` if the
+     * [MutableDoubleList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: DoubleArray): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableDoubleList].
+     */
+    public operator fun plusAssign(elements: DoubleList) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableDoubleList].
+     */
+    public operator fun plusAssign(elements: DoubleArray) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Removes all elements in the [MutableDoubleList]. The storage isn't released.
+     * @see trim
+     */
+    public fun clear() {
+        _size = 0
+    }
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public fun trim(minCapacity: Int = _size) {
+        val minSize = maxOf(minCapacity, _size)
+        if (capacity > minSize) {
+            content = content.copyOf(minSize)
+        }
+    }
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutableDoubleList].
+     * @see trim
+     */
+    public fun ensureCapacity(capacity: Int) {
+        val oldContent = content
+        if (oldContent.size < capacity) {
+            val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+            content = oldContent.copyOf(newSize)
+        }
+    }
+
+    /**
+     * [add] [element] to the [MutableDoubleList].
+     */
+    public inline operator fun plusAssign(element: Double) {
+        add(element)
+    }
+
+    /**
+     * [remove] [element] from the [MutableDoubleList]
+     */
+    public inline operator fun minusAssign(element: Double) {
+        remove(element)
+    }
+
+    /**
+     * Removes [element] from the [MutableDoubleList]. If [element] was in the [MutableDoubleList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public fun remove(element: Double): Boolean {
+        val index = indexOf(element)
+        if (index >= 0) {
+            removeAt(index)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all [elements] from the [MutableDoubleList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: DoubleArray): Boolean {
+        val initialSize = _size
+        for (i in elements.indices) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableDoubleList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: DoubleList): Boolean {
+        val initialSize = _size
+        for (i in 0..elements.lastIndex) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableDoubleList].
+     */
+    public operator fun minusAssign(elements: DoubleArray) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableDoubleList].
+     */
+    public operator fun minusAssign(elements: DoubleList) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): Double {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        val content = content
+        val item = content[index]
+        if (index != lastIndex) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index,
+                startIndex = index + 1,
+                endIndex = _size
+            )
+        }
+        _size--
+        return item
+    }
+
+    /**
+     * Removes items from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) {
+        if (start !in 0.._size || end !in 0.._size) {
+            throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+        }
+        if (end < start) {
+            throw IllegalArgumentException("Start ($start) is more than end ($end)")
+        }
+        if (end != start) {
+            if (end < _size) {
+                content.copyInto(
+                    destination = content,
+                    destinationOffset = start,
+                    startIndex = end,
+                    endIndex = _size
+                )
+            }
+            _size -= (end - start)
+        }
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableDoubleList] and removes all other values.
+     * @return `true` if the [MutableDoubleList] has changed.
+     */
+    public fun retainAll(elements: DoubleArray): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (elements.indexOfFirst { it == item } < 0) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableDoubleList] and removes all other values.
+     * @return `true` if the [MutableDoubleList] has changed.
+     */
+    public fun retainAll(elements: DoubleList): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (item !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: Double
+    ): Double {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+        }
+        val content = content
+        val old = content[index]
+        content[index] = element
+        return old
+    }
+
+    /**
+     * Sorts the [MutableDoubleList] elements in ascending order.
+     */
+    public fun sort() {
+        content.sort(fromIndex = 0, toIndex = _size)
+    }
+
+    /**
+     * Sorts the [MutableDoubleList] elements in descending order.
+     */
+    public fun sortDescending() {
+        content.sortDescending(fromIndex = 0, toIndex = _size)
+    }
+}
+
+private val EmptyDoubleList: DoubleList = MutableDoubleList(0)
+
+/**
+ * @return a read-only [DoubleList] with nothing in it.
+ */
+public fun emptyDoubleList(): DoubleList = EmptyDoubleList
+
+/**
+ * @return a read-only [DoubleList] with nothing in it.
+ */
+public fun doubleListOf(): DoubleList = EmptyDoubleList
+
+/**
+ * @return a new read-only [DoubleList] with [element1] as the only item in the list.
+ */
+public fun doubleListOf(element1: Double): DoubleList = mutableDoubleListOf(element1)
+
+/**
+ * @return a new read-only [DoubleList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun doubleListOf(element1: Double, element2: Double): DoubleList =
+    mutableDoubleListOf(element1, element2)
+
+/**
+ * @return a new read-only [DoubleList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun doubleListOf(element1: Double, element2: Double, element3: Double): DoubleList =
+    mutableDoubleListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [DoubleList] with [elements] in order.
+ */
+public fun doubleListOf(vararg elements: Double): DoubleList =
+    MutableDoubleList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * @return a new empty [MutableDoubleList] with the default capacity.
+ */
+public inline fun mutableDoubleListOf(): MutableDoubleList = MutableDoubleList()
+
+/**
+ * @return a new [MutableDoubleList] with [element1] as the only item in the list.
+ */
+public fun mutableDoubleListOf(element1: Double): MutableDoubleList {
+    val list = MutableDoubleList(1)
+    list += element1
+    return list
+}
+
+/**
+ * @return a new [MutableDoubleList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun mutableDoubleListOf(element1: Double, element2: Double): MutableDoubleList {
+    val list = MutableDoubleList(2)
+    list += element1
+    list += element2
+    return list
+}
+
+/**
+ * @return a new [MutableDoubleList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun mutableDoubleListOf(
+    element1: Double,
+    element2: Double,
+    element3: Double
+): MutableDoubleList {
+    val list = MutableDoubleList(3)
+    list += element1
+    list += element2
+    list += element3
+    return list
+}
+
+/**
+ * @return a new [MutableDoubleList] with the given elements, in order.
+ */
+public inline fun mutableDoubleListOf(vararg elements: Double): MutableDoubleList =
+    MutableDoubleList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleSet.kt
new file mode 100644
index 0000000..dd5d348
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleSet.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 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.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "KotlinRedundantDiagnosticSuppress",
+    "KotlinConstantConditions",
+    "PropertyName",
+    "ConstPropertyName",
+    "PrivatePropertyName",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// An empty array of doubles
+internal val EmptyDoubleArray = DoubleArray(0)
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
new file mode 100644
index 0000000..3c5636d
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
@@ -0,0 +1,744 @@
+/*
+ * Copyright 2023 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class DoubleListTest {
+    private val list: MutableDoubleList = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutableDoubleListOf()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutableDoubleList(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutableDoubleListOf(1.0, 2.0, 3.0)
+        assertEquals(3, l.size)
+        assertEquals(1.0, l[0])
+        assertEquals(2.0, l[1])
+        assertEquals(3.0, l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(5.0)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        assertEquals(list, l2)
+        assertNotEquals(list, mutableDoubleListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(5.0)
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[${1.0}, ${2.0}, ${3.0}, ${4.0}, ${5.0}]", list.toString())
+        assertEquals("[]", mutableDoubleListOf().toString())
+    }
+
+    @Test
+    fun joinToString() {
+        assertEquals("${1.0}, ${2.0}, ${3.0}, ${4.0}, ${5.0}", list.joinToString())
+        assertEquals(
+            "x${1.0}, ${2.0}, ${3.0}...",
+            list.joinToString(prefix = "x", postfix = "y", limit = 3)
+        )
+        assertEquals(
+            ">${1.0}-${2.0}-${3.0}-${4.0}-${5.0}<",
+            list.joinToString(separator = "-", prefix = ">", postfix = "<")
+        )
+        assertEquals("one, two, three...", list.joinToString(limit = 3) {
+            when (it.toInt()) {
+                1 -> "one"
+                2 -> "two"
+                3 -> "three"
+                else -> "whoops"
+            }
+        })
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutableDoubleListOf()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += 1.0
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(1.0, list[0])
+        assertEquals(5.0, list[4])
+        assertEquals(1.0, list.elementAt(0))
+        assertEquals(5.0, list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(1.0, list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            0.0
+        })
+        assertEquals(0.0, list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            0.0
+        })
+        assertEquals(0.0, list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            0.0
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it < 2.0 })
+        assertEquals(0, list.count { it < 0.0 })
+        assertEquals(5, list.count { it < 10.0 })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutableDoubleListOf().isEmpty())
+        assertTrue(mutableDoubleListOf().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutableDoubleListOf().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutableDoubleListOf().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == 5.0 })
+        assertTrue(list.any { it == 1.0 })
+        assertFalse(list.any { it == 0.0 })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutableDoubleListOf()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutableDoubleListOf(5.0, 4.0, 3.0, 2.0, 1.0)
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutableDoubleListOf()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(reversedSublist, mutableDoubleListOf(5.0, 4.0))
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutableDoubleListOf()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutableDoubleListOf()
+        list.forEachReversed { copy += it }
+        assertEquals(copy, mutableDoubleListOf(5.0, 4.0, 3.0, 2.0, 1.0))
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutableDoubleListOf()
+        val indices = mutableDoubleListOf()
+        list.forEachIndexed { index, item ->
+            copy += item
+            indices += index.toDouble()
+        }
+        assertEquals(list, copy)
+        assertEquals(indices, mutableDoubleListOf(0.0, 1.0, 2.0, 3.0, 4.0))
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutableDoubleListOf()
+        val indices = mutableDoubleListOf()
+        list.forEachReversedIndexed { index, item ->
+            copy += item
+            indices += index.toDouble()
+        }
+        assertEquals(copy, mutableDoubleListOf(5.0, 4.0, 3.0, 2.0, 1.0))
+        assertEquals(indices, mutableDoubleListOf(4.0, 3.0, 2.0, 1.0, 0.0))
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it == 1.0 })
+        assertEquals(4, list.indexOfFirst { it == 5.0 })
+        assertEquals(-1, list.indexOfFirst { it == 0.0 })
+        assertEquals(0, mutableDoubleListOf(8.0, 8.0).indexOfFirst { it == 8.0 })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it == 1.0 })
+        assertEquals(4, list.indexOfLast { it == 5.0 })
+        assertEquals(-1, list.indexOfLast { it == 0.0 })
+        assertEquals(1, mutableDoubleListOf(8.0, 8.0).indexOfLast { it == 8.0 })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(5.0))
+        assertTrue(list.contains(1.0))
+        assertFalse(list.contains(0.0))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(list.containsAll(mutableDoubleListOf(2.0, 3.0, 1.0)))
+        assertFalse(list.containsAll(mutableDoubleListOf(2.0, 3.0, 6.0)))
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(5.0))
+        assertEquals(1, list.lastIndexOf(2.0))
+        val copy = mutableDoubleListOf()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(1.0))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(1.0, list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableDoubleListOf().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(5.0, list.first { it == 5.0 })
+        assertEquals(1.0, mutableDoubleListOf(1.0, 5.0).first { it != 0.0 })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableDoubleListOf().first { it == 8.0 }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(5.0, list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableDoubleListOf().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(1.0, list.last { it == 1.0 })
+        assertEquals(5.0, mutableDoubleListOf(1.0, 5.0).last { it != 0.0 })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableDoubleListOf().last { it == 8.0 }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutableDoubleListOf(1.0, 2.0, 3.0)
+        l += 4.0
+        l.add(5.0)
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutableDoubleListOf(2.0, 4.0)
+        l.add(2, 5.0)
+        l.add(0, 1.0)
+        l.add(2, 3.0)
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, 2.0)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, 2.0)
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutableDoubleListOf(4.0)
+        val l2 = mutableDoubleListOf(1.0, 2.0)
+        val l3 = mutableDoubleListOf(5.0)
+        val l4 = mutableDoubleListOf(3.0)
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutableDoubleListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutableDoubleListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutableDoubleListOf())
+        }
+    }
+
+    @Test
+    fun addAllList() {
+        val l = MutableDoubleList()
+        l.add(3.0)
+        l.add(4.0)
+        l.add(5.0)
+        val l2 = mutableDoubleListOf(1.0, 2.0)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableDoubleListOf()))
+    }
+
+    @Test
+    fun plusAssignList() {
+        val l = MutableDoubleList()
+        l.add(3.0)
+        l.add(4.0)
+        l.add(5.0)
+        val l2 = mutableDoubleListOf(1.0, 2.0)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun addAllArrayAtIndex() {
+        val a1 = doubleArrayOf(4.0)
+        val a2 = doubleArrayOf(1.0, 2.0)
+        val a3 = doubleArrayOf(5.0)
+        val l = mutableDoubleListOf(3.0)
+        assertTrue(l.addAll(1, a3))
+        assertTrue(l.addAll(0, a2))
+        assertTrue(l.addAll(3, a1))
+        assertFalse(l.addAll(0, doubleArrayOf()))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(6, doubleArrayOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(-1, doubleArrayOf())
+        }
+    }
+
+    @Test
+    fun addAllArray() {
+        val a = doubleArrayOf(3.0, 4.0, 5.0)
+        val v = mutableDoubleListOf(1.0, 2.0)
+        v.addAll(a)
+        assertEquals(5, v.size)
+        assertEquals(3.0, v[2])
+        assertEquals(4.0, v[3])
+        assertEquals(5.0, v[4])
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val a = doubleArrayOf(3.0, 4.0, 5.0)
+        val v = mutableDoubleListOf(1.0, 2.0)
+        v += a
+        assertEquals(list, v)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutableDoubleListOf()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+    }
+
+    @Test
+    fun trim() {
+        val l = mutableDoubleListOf(1.0)
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        l -= 5.0
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        l.remove(3.0)
+        assertEquals(mutableDoubleListOf(1.0, 2.0, 4.0, 5.0), l)
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0)
+        l.removeAt(2)
+        assertEquals(mutableDoubleListOf(1.0, 2.0, 4.0, 5.0), l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutableDoubleListOf(0.0, 0.0, 0.0, 0.0, 0.0)
+        l[0] = 1.0
+        l[4] = 5.0
+        l[2] = 3.0
+        l[1] = 2.0
+        l[3] = 4.0
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(-1, 1.0)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(6, 1.0)
+        }
+        assertEquals(4.0, l.set(3, 1.0));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutableDoubleListOf(1.0)
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(list.removeAll(mutableDoubleListOf(0.0, 10.0, 15.0)))
+        val l = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0, 5.0)
+        assertTrue(l.removeAll(mutableDoubleListOf(20.0, 0.0, 15.0, 10.0, 5.0)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllDoubleArray() {
+        assertFalse(list.removeAll(doubleArrayOf(0.0, 10.0, 15.0)))
+        val l = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0, 5.0)
+        assertTrue(l.removeAll(doubleArrayOf(20.0, 0.0, 15.0, 10.0, 5.0)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutableDoubleListOf().also { it += list }
+        l -= mutableDoubleListOf(0.0, 10.0, 15.0)
+        assertEquals(list, l)
+        val l2 = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0, 5.0)
+        l2 -= mutableDoubleListOf(20.0, 0.0, 15.0, 10.0, 5.0)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignDoubleArray() {
+        val l = mutableDoubleListOf().also { it += list }
+        l -= doubleArrayOf(0.0, 10.0, 15.0)
+        assertEquals(list, l)
+        val l2 = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0, 5.0)
+        l2 -= doubleArrayOf(20.0, 0.0, 15.0, 10.0, 5.0)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(list.retainAll(mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)))
+        val l = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0)
+        assertTrue(l.retainAll(mutableDoubleListOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllDoubleArray() {
+        assertFalse(list.retainAll(doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)))
+        val l = mutableDoubleListOf(0.0, 1.0, 15.0, 10.0, 2.0, 3.0, 4.0, 5.0, 20.0)
+        assertTrue(l.retainAll(doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutableDoubleListOf(1.0, 9.0, 7.0, 6.0, 2.0, 3.0, 4.0, 5.0)
+        l.removeRange(1, 4)
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun sort() {
+        val l = mutableDoubleListOf(1.0, 4.0, 2.0, 5.0, 3.0)
+        l.sort()
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun sortDescending() {
+        val l = mutableDoubleListOf(1.0, 4.0, 2.0, 5.0, 3.0)
+        l.sortDescending()
+        assertEquals(mutableDoubleListOf(5.0, 4.0, 3.0, 2.0, 1.0), l)
+    }
+
+    @Test
+    fun testEmptyDoubleList() {
+        val l = emptyDoubleList()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun doubleListOfEmpty() {
+        val l = doubleListOf()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun doubleListOfOneValue() {
+        val l = doubleListOf(2.0)
+        assertEquals(1, l.size)
+        assertEquals(2.0, l[0])
+    }
+
+    @Test
+    fun doubleListOfTwoValues() {
+        val l = doubleListOf(2.0, 1.0)
+        assertEquals(2, l.size)
+        assertEquals(2.0, l[0])
+        assertEquals(1.0, l[1])
+    }
+
+    @Test
+    fun doubleListOfThreeValues() {
+        val l = doubleListOf(2.0, 10.0, -1.0)
+        assertEquals(3, l.size)
+        assertEquals(2.0, l[0])
+        assertEquals(10.0, l[1])
+        assertEquals(-1.0, l[2])
+    }
+
+    @Test
+    fun doubleListOfFourValues() {
+        val l = doubleListOf(2.0, 10.0, -1.0, 10.0)
+        assertEquals(4, l.size)
+        assertEquals(2.0, l[0])
+        assertEquals(10.0, l[1])
+        assertEquals(-1.0, l[2])
+        assertEquals(10.0, l[3])
+    }
+
+    @Test
+    fun mutableDoubleListOfOneValue() {
+        val l = mutableDoubleListOf(2.0)
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(2.0, l[0])
+    }
+
+    @Test
+    fun mutableDoubleListOfTwoValues() {
+        val l = mutableDoubleListOf(2.0, 1.0)
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(2.0, l[0])
+        assertEquals(1.0, l[1])
+    }
+
+    @Test
+    fun mutableDoubleListOfThreeValues() {
+        val l = mutableDoubleListOf(2.0, 10.0, -1.0)
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(2.0, l[0])
+        assertEquals(10.0, l[1])
+        assertEquals(-1.0, l[2])
+    }
+
+    @Test
+    fun mutableDoubleListOfFourValues() {
+        val l = mutableDoubleListOf(2.0, 10.0, -1.0, 10.0)
+        assertEquals(4, l.size)
+        assertEquals(4, l.capacity)
+        assertEquals(2.0, l[0])
+        assertEquals(10.0, l[1])
+        assertEquals(-1.0, l[2])
+        assertEquals(10.0, l[3])
+    }
+}
diff --git a/collection/collection/template/generateCollections.sh b/collection/collection/template/generateCollections.sh
index 99b1d7d..94667f7f 100755
--- a/collection/collection/template/generateCollections.sh
+++ b/collection/collection/template/generateCollections.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
-primitives=("Float" "Long" "Int")
-suffixes=("f" "L" "")
+primitives=("Double" "Float" "Long" "Int")
+suffixes=(".0" "f" "L" "")
 
 # Note: Had to use `dirname ${0}` on Linux
 scriptDir=`dirname ${PWD}/${0}`
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml b/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml
index cae21f8..87a3f9e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/AndroidManifest.xml
@@ -22,6 +22,12 @@
             android:name="androidx.compose.foundation.TestActivity"
             android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
         <activity
+            android:name="androidx.compose.foundation.TestActivityWithScreenLayoutConfigChanges"
+            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density"
+            android:exported="true"
+            android:theme="@android:style/Theme.Material.Light.NoActionBar"
+            />
+        <activity
             android:name="androidx.fragment.app.FragmentActivity"
             android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
         <!--
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableWithDynamicConfigChangesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableWithDynamicConfigChangesTest.kt
new file mode 100644
index 0000000..72e74a5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableWithDynamicConfigChangesTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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
+
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ClickableWithDynamicConfigChangesTest {
+
+    @get:Rule
+    val rule: ComposeContentTestRule =
+        createAndroidComposeRule<TestActivityWithScreenLayoutConfigChanges>()
+
+    @Test
+    fun click_viewAddedAndRemovedWithRecomposerCancelledAndRecreated_clickStillWorks() {
+        lateinit var grandParent: ViewGroup
+        lateinit var parentComposeView: ComposeView
+
+        var counter = 0
+
+        rule.setContent {
+            val view = LocalView.current
+            parentComposeView = view.parent as ComposeView
+            grandParent = parentComposeView.parent as ViewGroup
+
+            Box {
+                BasicText(
+                    "ClickableText",
+                    modifier = Modifier
+                        .testTag("myClickable")
+                        .clickable {
+                            ++counter
+                        }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(1)
+        }
+
+        rule.runOnUiThread {
+            grandParent.removeView(parentComposeView)
+        }
+
+        rule.runOnUiThread {
+            (rule as? AndroidComposeTestRule<*, *>)?.cancelAndRecreateRecomposer()
+        }
+
+        rule.runOnUiThread {
+            // THIS is the right one to cancel!
+            parentComposeView.setParentCompositionContext(null)
+        }
+
+        rule.runOnUiThread {
+            grandParent.addView(parentComposeView)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(2)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TestActivityWithScreenLayoutConfigChanges.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TestActivityWithScreenLayoutConfigChanges.kt
new file mode 100644
index 0000000..f381b91
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/TestActivityWithScreenLayoutConfigChanges.kt
@@ -0,0 +1,20 @@
+/*
+ * 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
+
+import androidx.activity.ComponentActivity
+
+class TestActivityWithScreenLayoutConfigChanges : ComponentActivity()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
index d86a98d..58a4c3f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -1458,6 +1458,47 @@
             assertThat(tweenAnimationSpec.animationWasExecutions).isEqualTo(1)
         }
 
+    @Test
+    fun anchoredDraggable_settleWhenOffsetEqualsTargetOffset() =
+        runBlocking(AutoTestFrameClock()) {
+            val inspectDecayAnimationSpec =
+                InspectSplineAnimationSpec(SplineBasedFloatDecayAnimationSpec(rule.density))
+            val decayAnimationSpec: DecayAnimationSpec<Float> =
+                inspectDecayAnimationSpec.generateDecayAnimationSpec()
+            val tweenAnimationSpec = InspectSpringAnimationSpec(tween(easing = LinearEasing))
+            val state = AnchoredDraggableState(
+                initialValue = A,
+                positionalThreshold = defaultPositionalThreshold,
+                velocityThreshold = defaultVelocityThreshold,
+                snapAnimationSpec = tweenAnimationSpec,
+                decayAnimationSpec = decayAnimationSpec,
+                anchors = DraggableAnchors {
+                    A at 0f
+                    B at 250f
+                }
+            )
+
+            val positionA = state.anchors.positionOf(A)
+            val positionB = state.anchors.positionOf(B)
+            val distance = abs(positionA - positionB)
+
+            assertThat(state.currentValue).isEqualTo(A)
+            assertThat(state.offset).isEqualTo(positionA)
+
+            state.dispatchRawDelta(distance)
+
+            assertThat(state.offset).isEqualTo(positionB)
+
+            state.settle(velocity = 1000f)
+
+            // Assert that the component settled at positionB (anchor B)
+            assertThat(state.offset).isEqualTo(positionB)
+
+            // since offset == positionB, decay animation is used
+            assertThat(inspectDecayAnimationSpec.animationWasExecutions).isEqualTo(1)
+            assertThat(tweenAnimationSpec.animationWasExecutions).isEqualTo(0)
+        }
+
     private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> { }
 
     private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
index 8b2d968..917a353 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.platform.UriHandler
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.test.assertIsFocused
@@ -248,6 +249,39 @@
         }
     }
 
+    @Test
+    fun link_withTranslatedString() {
+        val originalText = buildAnnotatedString {
+            append("text ")
+            withAnnotation(UrlAnnotation(Url1)) {
+                append("link")
+            }
+        }
+        setupContent { BasicText(originalText) }
+
+        // set translated string
+        val node = rule.onFirstText().fetchSemanticsNode()
+        rule.runOnUiThread {
+            val translatedText = buildAnnotatedString { append("text") }
+            node.config[SemanticsActions.SetTextSubstitution].action?.invoke(translatedText)
+        }
+        rule.waitForIdle()
+
+        rule.runOnUiThread {
+            // show the translated text
+            node.config[SemanticsActions.ShowTextSubstitution].action?.invoke(true)
+        }
+        rule.waitForIdle()
+
+        // check that there no link anymore
+        rule.onNode(hasClickAction()).assertDoesNotExist()
+
+        rule.onFirstText().performClick()
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(null)
+        }
+    }
+
     @Composable
     private fun TextWithLinks() = with(rule.density) {
         Column {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index 7f425a9..e6f22db 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -751,9 +751,10 @@
         if (!targetOffset.isNaN()) {
             var prev = if (offset.isNaN()) 0f else offset
             // If targetOffset is not in the same direction as the direction of the drag (sign
-            // of the velocity), velocity can't be used for decay animation. So, target animation
-            // should be used in this case.
-            if (sign(velocity) != sign(targetOffset - offset) || velocity == 0f) {
+            // of the velocity) we fall back to using target animation.
+            // If the component is at the target offset already, we use decay animation that will
+            // not consume any velocity.
+            if (velocity * (targetOffset - prev) < 0f || velocity == 0f) {
                 animateTo(velocity, this, anchors, latestTarget)
                 remainingVelocity = 0f
             } else {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index 9dbce7e..6089a6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.text.modifiers.SelectableTextAnnotatedStringElement
 import androidx.compose.foundation.text.modifiers.SelectionController
 import androidx.compose.foundation.text.modifiers.TextAnnotatedStringElement
+import androidx.compose.foundation.text.modifiers.TextAnnotatedStringNode
 import androidx.compose.foundation.text.modifiers.TextStringSimpleElement
 import androidx.compose.foundation.text.modifiers.hasLinks
 import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
@@ -27,10 +28,12 @@
 import androidx.compose.foundation.text.selection.hasSelection
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.ColorProducer
@@ -129,7 +132,8 @@
                 placeholders = null,
                 onPlaceholderLayout = null,
                 selectionController = selectionController,
-                color = color
+                color = color,
+                onShowTranslation = null
             )
     } else {
         modifier
@@ -228,16 +232,20 @@
                     placeholders = null,
                     onPlaceholderLayout = null,
                     selectionController = selectionController,
-                    color = color
+                    color = color,
+                    onShowTranslation = null
                 ),
             EmptyMeasurePolicy
         )
     } else {
+        // takes into account text substitution (for translation) that is happening inside the
+        // TextAnnotatedStringNode
+        var displayedText by remember(text) { mutableStateOf(text) }
+
         LayoutWithLinksAndInlineContent(
             modifier = modifier,
-            text = text,
+            text = displayedText,
             onTextLayout = onTextLayout,
-            hasLinks = hasLinks,
             hasInlineContent = hasInlineContent,
             inlineContent = inlineContent,
             style = style,
@@ -247,7 +255,14 @@
             minLines = minLines,
             fontFamilyResolver = LocalFontFamilyResolver.current,
             selectionController = selectionController,
-            color = color
+            color = color,
+            onShowTranslation = { substitutionValue ->
+                displayedText = if (substitutionValue.isShowingSubstitution) {
+                    substitutionValue.substitution
+                } else {
+                    substitutionValue.original
+                }
+            }
         )
     }
 }
@@ -359,6 +374,7 @@
 
 /** Measure policy for inline content and links */
 private class TextMeasurePolicy(
+    private val shouldMeasureLinks: () -> Boolean,
     private val placements: () -> List<Rect?>?
 ) : MeasurePolicy {
     override fun MeasureScope.measure(
@@ -389,7 +405,10 @@
         val linksMeasurables = measurables.fastFilter {
             it.parentData is TextRangeLayoutModifier
         }
-        val linksToPlace = measureWithTextRangeMeasureConstraints(linksMeasurables)
+        val linksToPlace = measureWithTextRangeMeasureConstraints(
+            measurables = linksMeasurables,
+            shouldMeasureLinks = shouldMeasureLinks
+        )
 
         return layout(constraints.maxWidth, constraints.maxHeight) {
             // inline content
@@ -397,7 +416,7 @@
                 placeable.place(position)
             }
             // links
-            linksToPlace.fastForEach { (placeable, measureResult) ->
+            linksToPlace?.fastForEach { (placeable, measureResult) ->
                 placeable.place(measureResult?.invoke() ?: IntOffset.Zero)
             }
         }
@@ -405,14 +424,19 @@
 }
 
 /** Measure policy for links only */
-private object LinksTextMeasurePolicy : MeasurePolicy {
+private class LinksTextMeasurePolicy(
+    private val shouldMeasureLinks: () -> Boolean
+) : MeasurePolicy {
     override fun MeasureScope.measure(
         measurables: List<Measurable>,
         constraints: Constraints
     ): MeasureResult {
-        val linksToPlace = measureWithTextRangeMeasureConstraints(measurables)
         return layout(constraints.maxWidth, constraints.maxHeight) {
-            linksToPlace.fastForEach { (placeable, measureResult) ->
+            val linksToPlace = measureWithTextRangeMeasureConstraints(
+                measurables = measurables,
+                shouldMeasureLinks = shouldMeasureLinks
+            )
+            linksToPlace?.fastForEach { (placeable, measureResult) ->
                 placeable.place(measureResult?.invoke() ?: IntOffset.Zero)
             }
         }
@@ -420,18 +444,24 @@
 }
 
 private fun measureWithTextRangeMeasureConstraints(
-    measurables: List<Measurable>
-): List<Pair<Placeable, (() -> IntOffset)?>> {
-    val textRangeLayoutMeasureScope = TextRangeLayoutMeasureScope()
-    return measurables.fastMapIndexedNotNull { _, measurable ->
-        val rangeMeasurePolicy = (measurable.parentData as TextRangeLayoutModifier).measurePolicy
-        val rangeMeasureResult = with(rangeMeasurePolicy) {
-            textRangeLayoutMeasureScope.measure()
+    measurables: List<Measurable>,
+    shouldMeasureLinks: () -> Boolean,
+): List<Pair<Placeable, (() -> IntOffset)?>>? {
+    return if (shouldMeasureLinks()) {
+        val textRangeLayoutMeasureScope = TextRangeLayoutMeasureScope()
+        measurables.fastMapIndexedNotNull { _, measurable ->
+            val rangeMeasurePolicy =
+                (measurable.parentData as TextRangeLayoutModifier).measurePolicy
+            val rangeMeasureResult = with(rangeMeasurePolicy) {
+                textRangeLayoutMeasureScope.measure()
+            }
+            val placeable = measurable.measure(
+                Constraints.fixed(rangeMeasureResult.width, rangeMeasureResult.height)
+            )
+            Pair(placeable, rangeMeasureResult.place)
         }
-        val placeable = measurable.measure(
-            Constraints.fixed(rangeMeasureResult.width, rangeMeasureResult.height)
-        )
-        Pair(placeable, rangeMeasureResult.place)
+    } else {
+        null
     }
 }
 
@@ -447,7 +477,8 @@
     placeholders: List<AnnotatedString.Range<Placeholder>>?,
     onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
     selectionController: SelectionController?,
-    color: ColorProducer?
+    color: ColorProducer?,
+    onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)?
 ): Modifier {
     if (selectionController == null) {
         val staticTextModifier = TextAnnotatedStringElement(
@@ -462,7 +493,8 @@
             placeholders,
             onPlaceholderLayout,
             null,
-            color
+            color,
+            onShowTranslation
         )
         return this then Modifier /* selection position */ then staticTextModifier
     } else {
@@ -489,7 +521,6 @@
     modifier: Modifier,
     text: AnnotatedString,
     onTextLayout: ((TextLayoutResult) -> Unit)?,
-    hasLinks: Boolean,
     hasInlineContent: Boolean,
     inlineContent: Map<String, InlineTextContent> = mapOf(),
     style: TextStyle,
@@ -499,9 +530,10 @@
     minLines: Int,
     fontFamilyResolver: FontFamily.Resolver,
     selectionController: SelectionController?,
-    color: ColorProducer?
+    color: ColorProducer?,
+    onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)?
 ) {
-    val textScope = if (hasLinks) {
+    val textScope = if (text.hasLinks()) {
         remember(text) { TextLinkScope(text) }
     } else null
 
@@ -545,12 +577,18 @@
                 placeholders = placeholders,
                 onPlaceholderLayout = onPlaceholderLayout,
                 selectionController = selectionController,
-                color = color
+                color = color,
+                onShowTranslation = onShowTranslation
             ),
         measurePolicy = if (!hasInlineContent) {
-            LinksTextMeasurePolicy
+            LinksTextMeasurePolicy(
+                shouldMeasureLinks = { textScope?.let { it.shouldMeasureLinks() } ?: false }
+            )
         } else {
-            TextMeasurePolicy { measuredPlaceholderPositions?.value }
+            TextMeasurePolicy(
+                shouldMeasureLinks = { textScope?.let { it.shouldMeasureLinks() } ?: false },
+                placements = { measuredPlaceholderPositions?.value }
+            )
         }
     )
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
index a34cb89..22a5e08 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
@@ -57,6 +57,12 @@
 internal class TextLinkScope(val text: AnnotatedString) {
     var textLayoutResult: TextLayoutResult? by mutableStateOf(null)
 
+    // indicates whether the links should be measured or not. The latter needed to handle
+    // case where translated string forces measurement before the recomposition. Recomposition in
+    // this case will dispose the links altogether because translator returns plain text
+    val shouldMeasureLinks: () -> Boolean
+        get() = { text == textLayoutResult?.layoutInput?.text }
+
     /**
      * Causes the modified element to be measured with fixed constraints equal to the bounds of the
      * text range [[start], [end]) and placed over that range of text.
@@ -106,9 +112,14 @@
 
     @OptIn(ExperimentalFoundationApi::class)
     @Composable
-    fun LinksComposables() = with(this) {
+    fun LinksComposables() {
+        // we might be out of sync if translation happened but BasicText's children weren't
+        // recomposed yet
+        if (!shouldMeasureLinks()) return
+
         val indication = LocalIndication.current
         val uriHandler = LocalUriHandler.current
+
         val links = text.getUrlAnnotations(0, text.length)
         links.fastForEach { range ->
             val interactionSource = remember(range) { MutableInteractionSource() }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index 57adcad..282e349 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -56,7 +56,8 @@
     placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
     onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
     private var selectionController: SelectionController? = null,
-    overrideColor: ColorProducer? = null
+    overrideColor: ColorProducer? = null,
+    private var onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
 ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
 
     private val delegate = delegate(
@@ -72,7 +73,8 @@
             placeholders = placeholders,
             onPlaceholderLayout = onPlaceholderLayout,
             selectionController = selectionController,
-            overrideColor = overrideColor
+            overrideColor = overrideColor,
+            onShowTranslation = onShowTranslation
         )
     )
 
@@ -144,7 +146,8 @@
             callbacksChanged = delegate.updateCallbacks(
                 onTextLayout = onTextLayout,
                 onPlaceholderLayout = onPlaceholderLayout,
-                selectionController = selectionController
+                selectionController = selectionController,
+                onShowTranslation = onShowTranslation
             ),
         )
         this.selectionController = selectionController
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
index 4530d5a..f8853ad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
@@ -45,7 +45,8 @@
     private val placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
     private val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
     private val selectionController: SelectionController? = null,
-    private val color: ColorProducer? = null
+    private val color: ColorProducer? = null,
+    private val onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
 ) : ModifierNodeElement<TextAnnotatedStringNode>() {
 
     override fun create(): TextAnnotatedStringNode = TextAnnotatedStringNode(
@@ -60,7 +61,8 @@
         placeholders,
         onPlaceholderLayout,
         selectionController,
-        color
+        color,
+        onShowTranslation
     )
 
     override fun update(node: TextAnnotatedStringNode) {
@@ -81,7 +83,8 @@
             callbacksChanged = node.updateCallbacks(
                 onTextLayout = onTextLayout,
                 onPlaceholderLayout = onPlaceholderLayout,
-                selectionController = selectionController
+                selectionController = selectionController,
+                onShowTranslation = onShowTranslation
             )
         )
     }
@@ -100,6 +103,7 @@
         // these are equally unlikely to change
         if (fontFamilyResolver != other.fontFamilyResolver) return false
         if (onTextLayout != other.onTextLayout) return false
+        if (onShowTranslation != other.onShowTranslation) return false
         if (overflow != other.overflow) return false
         if (softWrap != other.softWrap) return false
         if (maxLines != other.maxLines) return false
@@ -125,6 +129,7 @@
         result = 31 * result + (onPlaceholderLayout?.hashCode() ?: 0)
         result = 31 * result + (selectionController?.hashCode() ?: 0)
         result = 31 * result + (color?.hashCode() ?: 0)
+        result = 31 * result + (onShowTranslation?.hashCode() ?: 0)
         return result
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index c945897..6d1d3f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -82,7 +82,8 @@
     private var placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
     private var onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
     private var selectionController: SelectionController? = null,
-    private var overrideColor: ColorProducer? = null
+    private var overrideColor: ColorProducer? = null,
+    private var onShowTranslation: ((TextSubstitutionValue) -> Unit)? = null
 ) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
     private var baselineCache: Map<AlignmentLine, Int>? = null
 
@@ -194,7 +195,8 @@
     fun updateCallbacks(
         onTextLayout: ((TextLayoutResult) -> Unit)?,
         onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
-        selectionController: SelectionController?
+        selectionController: SelectionController?,
+        onShowTranslation: ((TextSubstitutionValue) -> Unit)?
     ): Boolean {
         var changed = false
 
@@ -212,6 +214,11 @@
             this.selectionController = selectionController
             changed = true
         }
+
+        if (this.onShowTranslation != onShowTranslation) {
+            this.onShowTranslation = onShowTranslation
+            changed = true
+        }
         return changed
     }
 
@@ -347,6 +354,7 @@
             if (this@TextAnnotatedStringNode.textSubstitution == null) {
                 return@showTextSubstitution false
             }
+            onShowTranslation?.invoke(this@TextAnnotatedStringNode.textSubstitution!!)
 
             this@TextAnnotatedStringNode.textSubstitution?.isShowingSubstitution = it
 
@@ -534,7 +542,12 @@
             }
 
             // draw inline content and links indication
-            if (text.hasLinks() || !placeholders.isNullOrEmpty()) {
+            val hasLinks = if (textSubstitution?.isShowingSubstitution == true) {
+                false
+            } else {
+                text.hasLinks()
+            }
+            if (hasLinks || !placeholders.isNullOrEmpty()) {
                 drawContent()
             }
         }
diff --git a/compose/material3/material3-adaptive/api/current.txt b/compose/material3/material3-adaptive/api/current.txt
index f32d8b3..edc1637 100644
--- a/compose/material3/material3-adaptive/api/current.txt
+++ b/compose/material3/material3-adaptive/api/current.txt
@@ -70,7 +70,8 @@
 
   public final class ListDetailPaneScaffold_androidKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> listPane, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> detailPane);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> paneDestinationHistory, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
@@ -145,7 +146,8 @@
 
   public final class SupportingPaneScaffold_androidKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> mainPane);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> paneDestinationHistory, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldAdaptStrategies {
@@ -160,14 +162,17 @@
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public interface ThreePaneScaffoldNavigator {
     method public boolean canNavigateBack(optional boolean scaffoldValueMustChange);
     method public androidx.compose.material3.adaptive.ThreePaneScaffoldState getScaffoldState();
+    method public boolean isDestinationHistoryAware();
     method public boolean navigateBack(optional boolean popUntilScaffoldValueChange);
     method public void navigateTo(androidx.compose.material3.adaptive.ThreePaneScaffoldRole pane);
+    method public void setDestinationHistoryAware(boolean);
+    property public abstract boolean isDestinationHistoryAware;
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState;
   }
 
   public final class ThreePaneScaffoldNavigator_androidKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
@@ -210,7 +215,8 @@
   }
 
   public final class ThreePaneScaffoldValueKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, androidx.compose.material3.adaptive.ThreePaneScaffoldRole? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> destinationHistory);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class WindowAdaptiveInfo {
diff --git a/compose/material3/material3-adaptive/api/restricted_current.txt b/compose/material3/material3-adaptive/api/restricted_current.txt
index f32d8b3..edc1637 100644
--- a/compose/material3/material3-adaptive/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive/api/restricted_current.txt
@@ -70,7 +70,8 @@
 
   public final class ListDetailPaneScaffold_androidKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> listPane, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> detailPane);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateListDetailPaneScaffoldState(java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> paneDestinationHistory, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
@@ -145,7 +146,8 @@
 
   public final class SupportingPaneScaffold_androidKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.ThreePaneScaffoldScope,kotlin.Unit> mainPane);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole currentPaneDestination, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldState calculateSupportingPaneScaffoldState(java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> paneDestinationHistory, optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldAdaptStrategies {
@@ -160,14 +162,17 @@
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public interface ThreePaneScaffoldNavigator {
     method public boolean canNavigateBack(optional boolean scaffoldValueMustChange);
     method public androidx.compose.material3.adaptive.ThreePaneScaffoldState getScaffoldState();
+    method public boolean isDestinationHistoryAware();
     method public boolean navigateBack(optional boolean popUntilScaffoldValueChange);
     method public void navigateTo(androidx.compose.material3.adaptive.ThreePaneScaffoldRole pane);
+    method public void setDestinationHistoryAware(boolean);
+    property public abstract boolean isDestinationHistoryAware;
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldState scaffoldState;
   }
 
   public final class ThreePaneScaffoldNavigator_androidKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> initialDestinationHistory);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
@@ -210,7 +215,8 @@
   }
 
   public final class ThreePaneScaffoldValueKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, optional androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional androidx.compose.material3.adaptive.ThreePaneScaffoldRole? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, androidx.compose.material3.adaptive.ThreePaneScaffoldRole? currentDestination);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.ThreePaneScaffoldValue calculateThreePaneScaffoldValue(int maxHorizontalPartitions, androidx.compose.material3.adaptive.ThreePaneScaffoldAdaptStrategies adaptStrategies, java.util.List<? extends androidx.compose.material3.adaptive.ThreePaneScaffoldRole> destinationHistory);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class WindowAdaptiveInfo {
diff --git a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldNavigatorTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldNavigatorTest.kt
index 83c85bd..1599b52 100644
--- a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffoldNavigatorTest.kt
@@ -36,7 +36,7 @@
     val composeRule = createComposeRule()
 
     @Test
-    fun singlePaneLayout_navigateTo_makeFocusPaneExpanded() {
+    fun singlePaneLayout_navigateTo_makeDestinationPaneExpanded() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -49,21 +49,21 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Hidden)
             scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
         }
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isTrue()
         }
     }
 
     @Test
-    fun dualPaneLayout_navigateTo_keepFocusPaneExpanded() {
+    fun dualPaneLayout_navigateTo_keepDestinationPaneExpanded() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -76,21 +76,77 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
         }
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isFalse()
         }
     }
 
     @Test
-    fun singlePaneLayout_navigateBack_makeFocusPaneHidden() {
+    fun dualPaneLayout_navigateToExtra_hideListWhenNotHistoryAware() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+        var canNavigateBack by Delegates.notNull<Boolean>()
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberListDetailPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                isDestinationHistoryAware = false
+            )
+            canNavigateBack = scaffoldNavigator.canNavigateBack()
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.List]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Extra)
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.List]
+            ).isEqualTo(PaneAdaptedValue.Hidden)
+            assertThat(canNavigateBack).isTrue()
+        }
+    }
+
+    @Test
+    fun dualPaneLayout_navigateToExtra_keepListExpandedWhenHistoryAware() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+        var canNavigateBack by Delegates.notNull<Boolean>()
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberListDetailPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                isDestinationHistoryAware = true
+            )
+            canNavigateBack = scaffoldNavigator.canNavigateBack()
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.List]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Extra)
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.List]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            assertThat(canNavigateBack).isTrue()
+        }
+    }
+
+    @Test
+    fun singlePaneLayout_navigateBack_makeDestinationPaneHidden() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -107,7 +163,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isTrue()
             scaffoldNavigator.navigateBack()
@@ -115,7 +171,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Hidden)
             assertThat(canNavigateBack).isFalse()
         }
@@ -156,7 +212,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(scaffoldNavigator.canNavigateBack(false)).isTrue()
             scaffoldNavigator.navigateBack(false)
@@ -164,12 +220,96 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
         }
     }
 
     @Test
+    fun dualPaneLayout_enforceScaffoldChangeWhenHistoryAware_notSkipBackstackEntry() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberListDetailPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                initialDestinationHistory = listOf(
+                    ListDetailPaneScaffoldRole.Extra,
+                    ListDetailPaneScaffoldRole.List,
+                ),
+                isDestinationHistoryAware = true
+            )
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded
+            )
+            scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateBack()
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded
+            )
+        }
+    }
+
+    @Test
+    fun dualPaneLayout_enforceScaffoldChangeWhenNotHistoryAware_skipBackstackEntry() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberListDetailPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                initialDestinationHistory = listOf(
+                    ListDetailPaneScaffoldRole.Extra,
+                    ListDetailPaneScaffoldRole.List,
+                ),
+                isDestinationHistoryAware = false
+            )
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateBack()
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded
+            )
+        }
+    }
+
+    @Test
     fun singlePaneToDualPaneLayout_enforceScaffoldValueChange_cannotNavigateBack() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         val mockCurrentScaffoldDirective = mutableStateOf(MockSinglePaneScaffoldDirective)
@@ -185,7 +325,7 @@
         }
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[ListDetailPaneScaffoldRole.Detail]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             // Switches to dual pane
             mockCurrentScaffoldDirective.value = MockDualPaneScaffoldDirective
@@ -216,3 +356,14 @@
     verticalPartitionSpacerSize = 0.dp,
     excludedBounds = emptyList()
 )
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun ThreePaneScaffoldValue.assert(
+    expectedDetailPaneAdaptedValue: PaneAdaptedValue,
+    expectedListPaneAdaptedValue: PaneAdaptedValue,
+    expectedExtraPaneAdaptedValue: PaneAdaptedValue
+) {
+    assertThat(this[ListDetailPaneScaffoldRole.Detail]).isEqualTo(expectedDetailPaneAdaptedValue)
+    assertThat(this[ListDetailPaneScaffoldRole.List]).isEqualTo(expectedListPaneAdaptedValue)
+    assertThat(this[ListDetailPaneScaffoldRole.Extra]).isEqualTo(expectedExtraPaneAdaptedValue)
+}
diff --git a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffoldNavigatorTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffoldNavigatorTest.kt
index 34d2315..181d578 100644
--- a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffoldNavigatorTest.kt
@@ -36,7 +36,7 @@
     val composeRule = createComposeRule()
 
     @Test
-    fun singlePaneLayout_navigateTo_makeFocusPaneExpanded() {
+    fun singlePaneLayout_navigateTo_makeDestinationPaneExpanded() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -49,21 +49,21 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.secondary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
             ).isEqualTo(PaneAdaptedValue.Hidden)
             scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting)
         }
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.secondary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isTrue()
         }
     }
 
     @Test
-    fun dualPaneLayout_navigateTo_keepFocusPaneExpanded() {
+    fun dualPaneLayout_navigateTo_keepDestinationPaneExpanded() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -76,21 +76,79 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Main]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
         }
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Main]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isFalse()
         }
     }
 
     @Test
-    fun singlePaneLayout_navigateBack_makeFocusPaneHidden() {
+    fun dualPaneLayout_navigateToExtra_hideSupportingWhenNotHistoryAware() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+        var canNavigateBack by Delegates.notNull<Boolean>()
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberSupportingPaneScaffoldNavigator(
+                initialDestinationHistory = listOf(SupportingPaneScaffoldRole.Supporting),
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                isDestinationHistoryAware = false
+            )
+            canNavigateBack = scaffoldNavigator.canNavigateBack()
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra)
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
+            ).isEqualTo(PaneAdaptedValue.Hidden)
+            assertThat(canNavigateBack).isTrue()
+        }
+    }
+
+    @Test
+    fun dualPaneLayout_navigateToExtra_keepSupportingExpandedWhenHistoryAware() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+        var canNavigateBack by Delegates.notNull<Boolean>()
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberSupportingPaneScaffoldNavigator(
+                initialDestinationHistory = listOf(SupportingPaneScaffoldRole.Supporting),
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                isDestinationHistoryAware = true
+            )
+            canNavigateBack = scaffoldNavigator.canNavigateBack()
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra)
+        }
+
+        composeRule.runOnIdle {
+            assertThat(
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
+            ).isEqualTo(PaneAdaptedValue.Expanded)
+            assertThat(canNavigateBack).isTrue()
+        }
+    }
+
+    @Test
+    fun singlePaneLayout_navigateBack_makeDestinationPaneHidden() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         var canNavigateBack by Delegates.notNull<Boolean>()
 
@@ -107,7 +165,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.secondary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(canNavigateBack).isTrue()
             scaffoldNavigator.navigateBack()
@@ -115,7 +173,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.secondary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Supporting]
             ).isEqualTo(PaneAdaptedValue.Hidden)
             assertThat(canNavigateBack).isFalse()
         }
@@ -156,7 +214,7 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Main]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             assertThat(scaffoldNavigator.canNavigateBack(false)).isTrue()
             scaffoldNavigator.navigateBack(false)
@@ -164,12 +222,96 @@
 
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Main]
             ).isEqualTo(PaneAdaptedValue.Expanded)
         }
     }
 
     @Test
+    fun dualPaneLayout_enforceScaffoldChangeWhenHistoryAware_notSkipBackstackEntry() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberSupportingPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                initialDestinationHistory = listOf(
+                    SupportingPaneScaffoldRole.Extra,
+                    SupportingPaneScaffoldRole.Supporting,
+                ),
+                isDestinationHistoryAware = true
+            )
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded
+            )
+            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateBack()
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded
+            )
+        }
+    }
+
+    @Test
+    fun dualPaneLayout_enforceScaffoldChangeWhenNotHistoryAware_skipBackstackEntry() {
+        lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
+
+        composeRule.setContent {
+            scaffoldNavigator = rememberSupportingPaneScaffoldNavigator(
+                scaffoldDirective = MockDualPaneScaffoldDirective,
+                initialDestinationHistory = listOf(
+                    SupportingPaneScaffoldRole.Extra,
+                    SupportingPaneScaffoldRole.Supporting,
+                ),
+                isDestinationHistoryAware = false
+            )
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden
+            )
+            scaffoldNavigator.navigateBack()
+        }
+
+        composeRule.runOnIdle {
+            scaffoldNavigator.scaffoldState.scaffoldValue.assert(
+                PaneAdaptedValue.Expanded,
+                PaneAdaptedValue.Hidden,
+                PaneAdaptedValue.Expanded
+            )
+        }
+    }
+
+    @Test
     fun singlePaneToDualPaneLayout_enforceScaffoldValueChange_cannotNavigateBack() {
         lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator
         val mockCurrentScaffoldDirective = mutableStateOf(MockSinglePaneScaffoldDirective)
@@ -185,7 +327,7 @@
         }
         composeRule.runOnIdle {
             assertThat(
-                scaffoldNavigator.scaffoldState.scaffoldValue.primary
+                scaffoldNavigator.scaffoldState.scaffoldValue[SupportingPaneScaffoldRole.Main]
             ).isEqualTo(PaneAdaptedValue.Expanded)
             // Switches to dual pane
             mockCurrentScaffoldDirective.value = MockDualPaneScaffoldDirective
@@ -216,3 +358,16 @@
     verticalPartitionSpacerSize = 0.dp,
     excludedBounds = emptyList()
 )
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun ThreePaneScaffoldValue.assert(
+    expectedMainPaneAdaptedValue: PaneAdaptedValue,
+    expectedSupportingPaneAdaptedValue: PaneAdaptedValue,
+    expectedExtraPaneAdaptedValue: PaneAdaptedValue
+) {
+    assertThat(this[SupportingPaneScaffoldRole.Main]).isEqualTo(expectedMainPaneAdaptedValue)
+    assertThat(this[SupportingPaneScaffoldRole.Supporting]).isEqualTo(
+        expectedSupportingPaneAdaptedValue
+    )
+    assertThat(this[SupportingPaneScaffoldRole.Extra]).isEqualTo(expectedExtraPaneAdaptedValue)
+}
diff --git a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
index 004d147..1659ae5 100644
--- a/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
+++ b/compose/material3/material3-adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldScreenshotTest.kt
@@ -170,7 +170,9 @@
         currentWindowAdaptiveInfo()
     )
     val scaffoldValue = calculateThreePaneScaffoldValue(
-        scaffoldDirective.maxHorizontalPartitions
+        scaffoldDirective.maxHorizontalPartitions,
+        ThreePaneScaffoldDefaults.adaptStrategies(),
+        null
     )
     SampleThreePaneScaffold(
         scaffoldDirective,
@@ -186,7 +188,9 @@
         currentWindowAdaptiveInfo()
     )
     val scaffoldValue = calculateThreePaneScaffoldValue(
-        scaffoldDirective.maxHorizontalPartitions
+        scaffoldDirective.maxHorizontalPartitions,
+        ThreePaneScaffoldDefaults.adaptStrategies(),
+        null
     )
     SampleThreePaneScaffold(
         scaffoldDirective,
@@ -204,7 +208,9 @@
         currentWindowAdaptiveInfo()
     )
     val scaffoldValue = calculateThreePaneScaffoldValue(
-        scaffoldDirective.maxHorizontalPartitions
+        scaffoldDirective.maxHorizontalPartitions,
+        ThreePaneScaffoldDefaults.adaptStrategies(),
+        null
     )
     SampleThreePaneScaffold(
         scaffoldDirective,
diff --git a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.android.kt b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.android.kt
index a45e62b..c4b2638 100644
--- a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.android.kt
+++ b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.android.kt
@@ -63,22 +63,22 @@
  * [ThreePaneScaffoldAdaptStrategies], and the current pane destination of a
  * [ListDetailPaneScaffold].
  *
+ * @param currentPaneDestination the current pane destination, which will be guaranteed to have the
+ *        highest priority when deciding pane visibilities.
  * @param scaffoldDirective the layout directives that the associated [ListDetailPaneScaffold]
  *        needs to follow. The default value will be the calculation result from
  *        [calculateStandardPaneScaffoldDirective] with the current window configuration, and
  *        will be automatically updated when the window configuration changes.
  * @param adaptStrategies the [ThreePaneScaffoldAdaptStrategies] should be used by scaffold panes.
- * @param currentPaneDestination the current pane destination, which will be guaranteed to have
- *        highest priority when deciding pane visibility.
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
 fun calculateListDetailPaneScaffoldState(
+    currentPaneDestination: ThreePaneScaffoldRole = ListDetailPaneScaffoldRole.List,
     scaffoldDirective: PaneScaffoldDirective =
         calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
     adaptStrategies: ThreePaneScaffoldAdaptStrategies =
-        ListDetailPaneScaffoldDefaults.adaptStrategies(),
-    currentPaneDestination: ThreePaneScaffoldRole = ListDetailPaneScaffoldRole.List
+        ListDetailPaneScaffoldDefaults.adaptStrategies()
 ): ThreePaneScaffoldState = ThreePaneScaffoldStateImpl(
     scaffoldDirective,
     calculateThreePaneScaffoldValue(
@@ -89,6 +89,40 @@
 )
 
 /**
+ * This function calculates [ThreePaneScaffoldValue] based on the given [PaneScaffoldDirective],
+ * [ThreePaneScaffoldAdaptStrategies], and the pane destination history of a
+ * [ListDetailPaneScaffold].
+ *
+ * @param paneDestinationHistory The history of past pane destinations, the last destination will
+ *        have the highest priority, and the second last destination will have the second highest
+ *        priority, and so forth until all panes has a priority assigned. Note that the last
+ *        destination is supposed to be the last item of the provided list. When the history is
+ *        empty or there are panes left unassigned, default priorities will be assigned to those
+ *        panes in the order of Detail > List > Extra.
+ * @param scaffoldDirective the layout directives that the associated [ListDetailPaneScaffold]
+ *        needs to follow. The default value will be the calculation result from
+ *        [calculateStandardPaneScaffoldDirective] with the current window configuration, and
+ *        will be automatically updated when the window configuration changes.
+ * @param adaptStrategies the [ThreePaneScaffoldAdaptStrategies] should be used by scaffold panes.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+fun calculateListDetailPaneScaffoldState(
+    paneDestinationHistory: List<ThreePaneScaffoldRole>,
+    scaffoldDirective: PaneScaffoldDirective =
+        calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
+    adaptStrategies: ThreePaneScaffoldAdaptStrategies =
+        ListDetailPaneScaffoldDefaults.adaptStrategies()
+): ThreePaneScaffoldState = ThreePaneScaffoldStateImpl(
+    scaffoldDirective,
+    calculateThreePaneScaffoldValue(
+        scaffoldDirective.maxHorizontalPartitions,
+        adaptStrategies,
+        paneDestinationHistory
+    )
+)
+
+/**
  * Provides default values of [ListDetailPaneScaffold].
  */
 @ExperimentalMaterial3AdaptiveApi
diff --git a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffold.android.kt b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffold.android.kt
index 98ddd70..757d437 100644
--- a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffold.android.kt
+++ b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/SupportingPaneScaffold.android.kt
@@ -64,22 +64,22 @@
  * [ThreePaneScaffoldAdaptStrategies], and the current pane destination of a
  * [SupportingPaneScaffold].
  *
+ * @param currentPaneDestination the current pane destination, which will be guaranteed to have the
+ *        highest priority when deciding pane visibilities.
  * @param scaffoldDirective the layout directives that the associated [SupportingPaneScaffold]
  *        needs to follow. The default value will be the calculation result from
  *        [calculateStandardPaneScaffoldDirective] with the current window configuration, and
  *        will be automatically updated when the window configuration changes.
  * @param adaptStrategies the [ThreePaneScaffoldAdaptStrategies] should be used by scaffold panes.
- * @param currentPaneDestination the current pane destination, which will be guaranteed to have
- *        highest priority when deciding pane visibility.
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
 fun calculateSupportingPaneScaffoldState(
+    currentPaneDestination: ThreePaneScaffoldRole = SupportingPaneScaffoldRole.Main,
     scaffoldDirective: PaneScaffoldDirective =
         calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
     adaptStrategies: ThreePaneScaffoldAdaptStrategies =
-        SupportingPaneScaffoldDefaults.adaptStrategies(),
-    currentPaneDestination: ThreePaneScaffoldRole = SupportingPaneScaffoldRole.Main
+        SupportingPaneScaffoldDefaults.adaptStrategies()
 ): ThreePaneScaffoldState = ThreePaneScaffoldStateImpl(
     scaffoldDirective,
     calculateThreePaneScaffoldValue(
@@ -90,6 +90,40 @@
 )
 
 /**
+ * This function calculates [ThreePaneScaffoldValue] based on the given [PaneScaffoldDirective],
+ * [ThreePaneScaffoldAdaptStrategies], and the pane destination history of a
+ * [SupportingPaneScaffold].
+ *
+ * @param paneDestinationHistory The history of past pane destinations, the last destination will
+ *        have the highest priority, and the second last destination will have the second highest
+ *        priority, and so forth until all panes has a priority assigned. Note that the last
+ *        destination is supposed to be the last item of the provided list. When the history is
+ *        empty or there are panes left unassigned, default priorities will be assigned to those
+ *        panes in the order of Main > Supporting > Extra.
+ * @param scaffoldDirective the layout directives that the associated [SupportingPaneScaffold]
+ *        needs to follow. The default value will be the calculation result from
+ *        [calculateStandardPaneScaffoldDirective] with the current window configuration, and
+ *        will be automatically updated when the window configuration changes.
+ * @param adaptStrategies the [ThreePaneScaffoldAdaptStrategies] should be used by scaffold panes.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+fun calculateSupportingPaneScaffoldState(
+    paneDestinationHistory: List<ThreePaneScaffoldRole>,
+    scaffoldDirective: PaneScaffoldDirective =
+        calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
+    adaptStrategies: ThreePaneScaffoldAdaptStrategies =
+        SupportingPaneScaffoldDefaults.adaptStrategies()
+): ThreePaneScaffoldState = ThreePaneScaffoldStateImpl(
+    scaffoldDirective,
+    calculateThreePaneScaffoldValue(
+        scaffoldDirective.maxHorizontalPartitions,
+        adaptStrategies,
+        paneDestinationHistory
+    )
+)
+
+/**
  * Provides default values of [SupportingPaneScaffold].
  */
 @ExperimentalMaterial3AdaptiveApi
diff --git a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldNavigator.android.kt b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldNavigator.android.kt
index 329f29a..49f8e40 100644
--- a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldNavigator.android.kt
+++ b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldNavigator.android.kt
@@ -51,6 +51,15 @@
     val scaffoldState: ThreePaneScaffoldState
 
     /**
+     * Indicates if the navigator should be aware of pane destination history when deciding the
+     * result [ThreePaneScaffoldValue] by a navigation operation. If the value is `false`, only
+     * the current destination will be considered in the scaffold value calculation.
+     *
+     * @see calculateThreePaneScaffoldValue for more detailed explanation about history awareness.
+     */
+    var isDestinationHistoryAware: Boolean
+
+    /**
      * Navigates to a new pane destination. The new destination is supposed to have the highest
      * priority when calculating the new [scaffoldState]. When implementing this method, please
      * ensure the new destination pane will be expanded or adapted in a reasonable way so it
@@ -93,6 +102,9 @@
  *        calculated with [calculateStandardPaneScaffoldDirective] using [WindowAdaptiveInfo]
  *        retrieved from the current context.
  * @param adaptStrategies adaptation strategies of each pane.
+ * @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
+ *        full destination history, instead of just the current destination. See
+ *        [calculateThreePaneScaffoldValue] for more relevant details.
  * @param initialDestinationHistory the initial pane destination history of the scaffold, by default
  *        it will be just the list pane.
  */
@@ -103,11 +115,13 @@
         calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
     adaptStrategies: ThreePaneScaffoldAdaptStrategies =
         ListDetailPaneScaffoldDefaults.adaptStrategies(),
+    isDestinationHistoryAware: Boolean = true,
     initialDestinationHistory: List<ThreePaneScaffoldRole> = listOf(ListDetailPaneScaffoldRole.List)
 ): ThreePaneScaffoldNavigator =
     rememberThreePaneScaffoldNavigator(
         scaffoldDirective,
         adaptStrategies,
+        isDestinationHistoryAware,
         initialDestinationHistory
     )
 
@@ -121,6 +135,9 @@
  *        calculated with [calculateStandardPaneScaffoldDirective] using [WindowAdaptiveInfo]
  *        retrieved from the current context.
  * @param adaptStrategies adaptation strategies of each pane.
+ * @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
+ *        full destination history, instead of just the current destination. See
+ *        [calculateThreePaneScaffoldValue] for more relevant details.
  * @param initialDestinationHistory the initial destination history of the scaffold, by default it
  *        will be just the main pane.
  */
@@ -131,12 +148,14 @@
         calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
     adaptStrategies: ThreePaneScaffoldAdaptStrategies =
         SupportingPaneScaffoldDefaults.adaptStrategies(),
+    isDestinationHistoryAware: Boolean = true,
     initialDestinationHistory: List<ThreePaneScaffoldRole> =
         listOf(SupportingPaneScaffoldRole.Main)
 ): ThreePaneScaffoldNavigator =
     rememberThreePaneScaffoldNavigator(
         scaffoldDirective,
         adaptStrategies,
+        isDestinationHistoryAware,
         initialDestinationHistory
     )
 
@@ -145,19 +164,26 @@
 internal fun rememberThreePaneScaffoldNavigator(
     scaffoldDirective: PaneScaffoldDirective,
     adaptStrategies: ThreePaneScaffoldAdaptStrategies,
+    isDestinationHistoryAware: Boolean,
     initialDestinationHistory: List<ThreePaneScaffoldRole>
 ): ThreePaneScaffoldNavigator =
     rememberSaveable(
-        saver = DefaultThreePaneScaffoldNavigator.saver(scaffoldDirective, adaptStrategies)
+        saver = DefaultThreePaneScaffoldNavigator.saver(
+            scaffoldDirective,
+            adaptStrategies,
+            isDestinationHistoryAware
+        )
     ) {
         DefaultThreePaneScaffoldNavigator(
             initialDestinationHistory = initialDestinationHistory,
             initialScaffoldDirective = scaffoldDirective,
-            initialAdaptStrategies = adaptStrategies
+            initialAdaptStrategies = adaptStrategies,
+            initialIsDestinationHistoryAware = isDestinationHistoryAware
         )
     }.apply {
         this.scaffoldDirective = scaffoldDirective
         this.adaptStrategies = adaptStrategies
+        this.isDestinationHistoryAware = isDestinationHistoryAware
     }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -165,6 +191,7 @@
     initialDestinationHistory: List<ThreePaneScaffoldRole>,
     initialScaffoldDirective: PaneScaffoldDirective,
     initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies,
+    initialIsDestinationHistoryAware: Boolean
 ) : ThreePaneScaffoldNavigator, ThreePaneScaffoldState {
 
     private val destinationHistory = mutableStateListOf<ThreePaneScaffoldRole>().apply {
@@ -175,12 +202,14 @@
 
     override var scaffoldDirective by mutableStateOf(initialScaffoldDirective)
 
+    override var isDestinationHistoryAware by mutableStateOf(initialIsDestinationHistoryAware)
+
     var adaptStrategies by mutableStateOf(initialAdaptStrategies)
 
     val currentDestination: ThreePaneScaffoldRole? get() = destinationHistory.lastOrNull()
 
     override val scaffoldValue by derivedStateOf {
-        calculateScaffoldValue(currentDestination)
+        calculateScaffoldValue(destinationHistory.lastIndex)
     }
 
     override fun navigateTo(pane: ThreePaneScaffoldRole) {
@@ -212,7 +241,7 @@
             return destinationHistory.lastIndex - 1
         }
         for (previousDestinationIndex in destinationHistory.lastIndex - 1 downTo 0) {
-            val newValue = calculateScaffoldValue(destinationHistory[previousDestinationIndex])
+            val newValue = calculateScaffoldValue(previousDestinationIndex)
             if (newValue != scaffoldValue) {
                 return previousDestinationIndex
             }
@@ -220,14 +249,26 @@
         return -1
     }
 
-    private fun calculateScaffoldValue(
-        destination: ThreePaneScaffoldRole?
-    ): ThreePaneScaffoldValue =
-        calculateThreePaneScaffoldValue(
-            scaffoldDirective.maxHorizontalPartitions,
-            adaptStrategies,
-            destination
-        )
+    private fun calculateScaffoldValue(destinationIndex: Int) =
+        if (destinationIndex == -1) {
+            calculateThreePaneScaffoldValue(
+                scaffoldDirective.maxHorizontalPartitions,
+                adaptStrategies,
+                null
+            )
+        } else if (isDestinationHistoryAware) {
+            calculateThreePaneScaffoldValue(
+                scaffoldDirective.maxHorizontalPartitions,
+                adaptStrategies,
+                destinationHistory.subList(0, destinationIndex + 1)
+            )
+        } else {
+            calculateThreePaneScaffoldValue(
+                scaffoldDirective.maxHorizontalPartitions,
+                adaptStrategies,
+                destinationHistory[destinationIndex]
+            )
+        }
 
     companion object {
         /**
@@ -235,7 +276,8 @@
          */
         fun saver(
             initialScaffoldDirective: PaneScaffoldDirective,
-            initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies
+            initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies,
+            initialDestinationHistoryAware: Boolean
         ): Saver<DefaultThreePaneScaffoldNavigator, *> = listSaver(
             save = {
                 it.destinationHistory
@@ -244,7 +286,8 @@
                 DefaultThreePaneScaffoldNavigator(
                     initialDestinationHistory = it,
                     initialScaffoldDirective = initialScaffoldDirective,
-                    initialAdaptStrategies = initialAdaptStrategies
+                    initialAdaptStrategies = initialAdaptStrategies,
+                    initialIsDestinationHistoryAware = initialDestinationHistoryAware
                 )
             }
         )
diff --git a/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
index 32cfb93..c751a23 100644
--- a/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
+++ b/compose/material3/material3-adaptive/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValueTest.kt
@@ -25,57 +25,122 @@
 @RunWith(JUnit4::class)
 class ThreePaneScaffoldValueTest {
     @Test
-    fun test_onePaneLayoutNoFocus() {
+    fun calculateWithoutHistory_onePaneLayout_noDestination() {
         val scaffoldState = calculateThreePaneScaffoldValue(
             maxHorizontalPartitions = 1,
-            adaptStrategies = MockAdaptStrategies
+            adaptStrategies = MockAdaptStrategies,
+            currentDestination = null
         )
         scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
-        scaffoldState.assertState(
-            ThreePaneScaffoldRole.Secondary,
-            SecondaryPaneAdaptedState
-        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, SecondaryPaneAdaptedState)
         scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
     }
 
     @Test
-    fun test_onePaneLayoutWithFocus() {
+    fun calculateWithHistory_onePaneLayout_noDestination() {
+        val scaffoldState = calculateThreePaneScaffoldValue(
+            maxHorizontalPartitions = 1,
+            adaptStrategies = MockAdaptStrategies,
+            destinationHistory = emptyList()
+        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, SecondaryPaneAdaptedState)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
+    }
+
+    @Test
+    fun calculateWithoutHistory_onePaneLayout() {
         val scaffoldState = calculateThreePaneScaffoldValue(
             maxHorizontalPartitions = 1,
             adaptStrategies = MockAdaptStrategies,
             currentDestination = ThreePaneScaffoldRole.Secondary
         )
         scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PrimaryPaneAdaptedState)
-        scaffoldState.assertState(
-            ThreePaneScaffoldRole.Secondary,
-            PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded)
         scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
     }
 
     @Test
-    fun test_twoPaneLayoutNoFocus() {
+    fun calculateWithHistory_onePaneLayout() {
+        val scaffoldState = calculateThreePaneScaffoldValue(
+            maxHorizontalPartitions = 1,
+            adaptStrategies = MockAdaptStrategies,
+            destinationHistory = listOf(
+                ThreePaneScaffoldRole.Tertiary,
+                ThreePaneScaffoldRole.Secondary
+            )
+        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PrimaryPaneAdaptedState)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
+    }
+
+    @Test
+    fun calculateWithoutHistory_twoPaneLayout_noDestination() {
         val scaffoldState = calculateThreePaneScaffoldValue(
             maxHorizontalPartitions = 2,
-            adaptStrategies = MockAdaptStrategies
+            adaptStrategies = MockAdaptStrategies,
+            currentDestination = null
         )
         scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
-        scaffoldState.assertState(
-            ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded
-        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded)
         scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
     }
 
     @Test
-    fun test_twoPaneLayoutWithFocus() {
+    fun calculateWithHistory_twoPaneLayout_noDestination() {
+        val scaffoldState = calculateThreePaneScaffoldValue(
+            maxHorizontalPartitions = 2,
+            adaptStrategies = MockAdaptStrategies,
+            destinationHistory = emptyList()
+        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, TertiaryPaneAdaptedState)
+    }
+
+    @Test
+    fun calculateWithoutHistory_twoPaneLayout() {
         val scaffoldState = calculateThreePaneScaffoldValue(
             maxHorizontalPartitions = 2,
             adaptStrategies = MockAdaptStrategies,
             currentDestination = ThreePaneScaffoldRole.Tertiary
         )
         scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
-        scaffoldState.assertState(
-            ThreePaneScaffoldRole.Secondary, SecondaryPaneAdaptedState
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, SecondaryPaneAdaptedState)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, PaneAdaptedValue.Expanded)
+    }
+
+    @Test
+    fun calculateWithHistory_twoPaneLayout() {
+        val scaffoldState = calculateThreePaneScaffoldValue(
+            maxHorizontalPartitions = 2,
+            adaptStrategies = MockAdaptStrategies,
+            destinationHistory = listOf(
+                ThreePaneScaffoldRole.Tertiary,
+                ThreePaneScaffoldRole.Secondary
+            )
         )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PrimaryPaneAdaptedState)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, PaneAdaptedValue.Expanded)
+    }
+
+    @Test
+    fun calculateWithHistory_twoPaneLayout_longHistory() {
+        val scaffoldState = calculateThreePaneScaffoldValue(
+            maxHorizontalPartitions = 2,
+            adaptStrategies = MockAdaptStrategies,
+            destinationHistory = listOf(
+                ThreePaneScaffoldRole.Primary,
+                ThreePaneScaffoldRole.Tertiary,
+                ThreePaneScaffoldRole.Secondary,
+                ThreePaneScaffoldRole.Primary,
+                ThreePaneScaffoldRole.Tertiary
+            )
+        )
+        scaffoldState.assertState(ThreePaneScaffoldRole.Primary, PaneAdaptedValue.Expanded)
+        scaffoldState.assertState(ThreePaneScaffoldRole.Secondary, SecondaryPaneAdaptedState)
         scaffoldState.assertState(ThreePaneScaffoldRole.Tertiary, PaneAdaptedValue.Expanded)
     }
 
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
index 981a5a7..6eaa462 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material3.adaptive
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.util.fastForEachReversed
 
 @ExperimentalMaterial3AdaptiveApi
 private inline fun buildThreePaneScaffoldValue(
@@ -41,7 +42,7 @@
  * [PaneAdaptedValue.Expanded], otherwise it will be adapted according to its associated
  * [AdaptStrategy].
  *
- * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.
+ * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
  *        how many expanded panes can be shown at the same time.
  * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
  *        the default value will be [ThreePaneScaffoldDefaults.threePaneScaffoldAdaptStrategies].
@@ -51,8 +52,8 @@
 @ExperimentalMaterial3AdaptiveApi
 fun calculateThreePaneScaffoldValue(
     maxHorizontalPartitions: Int,
-    adaptStrategies: ThreePaneScaffoldAdaptStrategies = ThreePaneScaffoldDefaults.adaptStrategies(),
-    currentDestination: ThreePaneScaffoldRole? = null,
+    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
+    currentDestination: ThreePaneScaffoldRole?,
 ): ThreePaneScaffoldValue {
     var expandedCount = if (currentDestination != null) 1 else 0
     return buildThreePaneScaffoldValue { role ->
@@ -69,6 +70,83 @@
 }
 
 /**
+ * Calculates the current adapted value of [ThreePaneScaffold] according to the given
+ * [maxHorizontalPartitions], [adaptStrategies] and [destinationHistory]. The returned value can be
+ * used as a unique representation of the current layout structure.
+ *
+ * The function will treat the current focus as the highest priority and then adapt the rest
+ * panes according to the order of [ThreePaneScaffoldRole.Primary],
+ * [ThreePaneScaffoldRole.Secondary] and [ThreePaneScaffoldRole.Tertiary]. If there are still
+ * remaining partitions to put the pane, the pane will be set as [PaneAdaptedValue.Expanded],
+ * otherwise it will be adapted according to its associated [AdaptStrategy].
+ *
+ * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
+ *        how many expanded panes can be shown at the same time.
+ * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
+ *        the default value will be [ThreePaneScaffoldDefaults.threePaneScaffoldAdaptStrategies].
+ * @param destinationHistory The history of past destination panes, the last destination will have
+ *        the highest priority, and the second last destination will have the second highest
+ *        priority, and so forth until all panes has a priority assigned. Note that the last
+ *        destination is supposed to be the last item of the provided list.
+ */
+@ExperimentalMaterial3AdaptiveApi
+fun calculateThreePaneScaffoldValue(
+    maxHorizontalPartitions: Int,
+    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
+    destinationHistory: List<ThreePaneScaffoldRole>,
+): ThreePaneScaffoldValue {
+    var expandedCount = 0
+    var primaryPaneAdaptedValue: PaneAdaptedValue? = null
+    var secondaryPaneAdaptedValue: PaneAdaptedValue? = null
+    var tertiaryPaneAdaptedValue: PaneAdaptedValue? = null
+    destinationHistory.fastForEachReversed {
+        if (expandedCount >= maxHorizontalPartitions) {
+            return@fastForEachReversed
+        }
+        when (it) {
+            ThreePaneScaffoldRole.Primary -> {
+                if (primaryPaneAdaptedValue == null) {
+                    primaryPaneAdaptedValue = PaneAdaptedValue.Expanded
+                    expandedCount++
+                }
+            }
+            ThreePaneScaffoldRole.Secondary -> {
+                if (secondaryPaneAdaptedValue == null) {
+                    secondaryPaneAdaptedValue = PaneAdaptedValue.Expanded
+                    expandedCount++
+                }
+            }
+            ThreePaneScaffoldRole.Tertiary -> {
+                if (tertiaryPaneAdaptedValue == null) {
+                    tertiaryPaneAdaptedValue = PaneAdaptedValue.Expanded
+                    expandedCount++
+                }
+            }
+        }
+    }
+    return ThreePaneScaffoldValue(
+        primary = primaryPaneAdaptedValue ?: if (expandedCount < maxHorizontalPartitions) {
+            expandedCount++
+            PaneAdaptedValue.Expanded
+        } else {
+            adaptStrategies[ThreePaneScaffoldRole.Primary].adapt()
+        },
+        secondary = secondaryPaneAdaptedValue ?: if (expandedCount < maxHorizontalPartitions) {
+            expandedCount++
+            PaneAdaptedValue.Expanded
+        } else {
+            adaptStrategies[ThreePaneScaffoldRole.Secondary].adapt()
+        },
+        tertiary = tertiaryPaneAdaptedValue ?: if (expandedCount < maxHorizontalPartitions) {
+            expandedCount++
+            PaneAdaptedValue.Expanded
+        } else {
+            adaptStrategies[ThreePaneScaffoldRole.Tertiary].adapt()
+        }
+    )
+}
+
+/**
  * The adapted value of [ThreePaneScaffold]. It contains each pane's adapted value.
  * [ThreePaneScaffold] will use the adapted values to decide which panes should be displayed
  * and how they should be displayed. With other input parameters of [ThreePaneScaffold] fixed,
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 700de78..27cac4b 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -41,6 +41,7 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
+                implementation("androidx.collection:collection:1.4.0-beta02")
                 implementation("androidx.compose.animation:animation-core:1.6.0-beta01")
 
                 api("androidx.compose.foundation:foundation:1.6.0-beta01")
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
index 8617997..f93a36c 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material3.tokens.IconButtonTokens
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -168,7 +169,7 @@
     }
 
     @Test
-    fun iconButtonColor_localContentColor() {
+    fun iconButtonColors_localContentColor() {
         rule.setMaterialContent(lightColorScheme()) {
             CompositionLocalProvider(LocalContentColor provides Color.Blue) {
                 val colors = IconButtonDefaults.iconButtonColors()
@@ -186,7 +187,30 @@
     }
 
     @Test
-    fun iconButtonColor_copy() {
+    fun iconButtonColors_customValues() {
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalContentColor provides Color.Blue) {
+                val colors = IconButtonDefaults.iconButtonColors()
+                assert(colors.contentColor == Color.Blue)
+                assert(colors.disabledContentColor
+                    == Color.Blue.copy(IconButtonTokens.DisabledIconOpacity))
+            }
+
+            CompositionLocalProvider(LocalContentColor provides Color.Red) {
+                val colors = IconButtonDefaults.iconButtonColors(
+                    containerColor = Color.Blue,
+                    contentColor = Color.Green
+                )
+                assert(colors.containerColor == Color.Blue)
+                assert(colors.contentColor == Color.Green)
+                assert(colors.disabledContentColor
+                    == Color.Green.copy(IconButtonTokens.DisabledIconOpacity))
+            }
+        }
+    }
+
+    @Test
+    fun iconButtonColors_copy() {
         rule.setMaterialContent(lightColorScheme()) {
             val colors = IconButtonDefaults.iconButtonColors().copy()
             assert(colors == IconButtonDefaults.iconButtonColors())
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt
new file mode 100644
index 0000000..e528318
--- /dev/null
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.material3.carousel
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class KeylineTest {
+
+    @Test
+    fun testKeylineList_findsFirstAndLastFocalKeylines() {
+        val keylineList = createTestKeylineList()
+        assertThat(keylineList.firstFocalIndex).isEqualTo(4)
+        assertThat(keylineList.lastFocalIndex).isEqualTo(5)
+    }
+
+    @Test
+    fun testKeylineList_pivotsWithCorrectCutoffsRight() {
+        val carouselMainAxisSize = LargeSize + (MediumSize * .75f).roundToInt()
+        val keylineList = keylineListOf(carouselMainAxisSize, CarouselAlignment.Start) {
+            add(SmallSize, isAnchor = true)
+            add(LargeSize)
+            add(MediumSize)
+            add(SmallSize, isAnchor = true)
+        }
+
+        assertThat(keylineList[2].cutoff)
+            .isEqualTo(MediumSize - (MediumSize * .75f).roundToInt())
+    }
+
+    @Test
+    fun testKeylineList_pivotsWithCorrectCutoffsLeft() {
+        val carouselMainAxisSize = (MediumSize * .75f).roundToInt() + LargeSize
+        val keylineList = keylineListOf(carouselMainAxisSize, CarouselAlignment.End) {
+            add(SmallSize, isAnchor = true)
+            add(MediumSize)
+            add(LargeSize)
+            add(SmallSize, isAnchor = true)
+        }
+
+        assertThat(keylineList[1].cutoff)
+            .isEqualTo(-MediumSize + (MediumSize * .75f).roundToInt())
+    }
+
+    @Test
+    fun testKeylineList_findsFirstIndexAfterFocalRangeWithSize() {
+        val keylineList = createTestKeylineList()
+        assertThat(keylineList.firstIndexAfterFocalRangeWithSize(SmallSize))
+            .isEqualTo(7)
+    }
+
+    @Test
+    fun testKeylineList_findsLastIndexBeforeFocalRangeWithSize() {
+        val keylineList = createTestKeylineList()
+        assertThat(keylineList.lastIndexBeforeFocalRangeWithSize(SmallSize))
+            .isEqualTo(2)
+    }
+
+    @Test
+    fun testKeylineList_getKeylineBefore() {
+        val keylineList = createTestKeylineList()
+
+        assertThat(keylineList.getKeylineBefore(unadjustedOffset = 60f)).isEqualTo(keylineList[3])
+        assertThat(keylineList.getKeylineBefore(-Float.MAX_VALUE)).isEqualTo(keylineList[0])
+        assertThat(keylineList.getKeylineBefore(Float.MAX_VALUE)).isEqualTo(keylineList.last())
+    }
+
+    @Test
+    fun testKeylineList_getKeylineAfter() {
+        val keylineList = createTestKeylineList()
+
+        assertThat(keylineList.getKeylineAfter(60f)).isEqualTo(keylineList[4])
+        assertThat(keylineList.getKeylineAfter(-Float.MAX_VALUE)).isEqualTo(keylineList[0])
+        assertThat(keylineList.getKeylineAfter(Float.MAX_VALUE)).isEqualTo(keylineList.last())
+    }
+
+    companion object {
+        private const val LargeSize = 100f
+        private const val SmallSize = 20f
+        private const val XSmallSize = 5f
+        private const val MediumSize = (LargeSize + SmallSize) / 2f
+
+        /**
+         * Creates a list of keylines with the following arrangement: [xs-s-s-m-l-l-m-s-s-xs].
+         */
+        private fun createTestKeylineList(): KeylineList {
+            // Arrangement:
+            // [xs-s-s-m-l-l-m-s-s-xs]
+            val carouselMainAxisSize =
+                (XSmallSize * 2) + (SmallSize * 4) + (MediumSize * 2) + (LargeSize * 2)
+            return keylineListOf(carouselMainAxisSize, CarouselAlignment.Center) {
+                add(XSmallSize, isAnchor = true)
+                add(SmallSize)
+                add(SmallSize)
+                add(MediumSize)
+                add(LargeSize)
+                add(LargeSize)
+                add(MediumSize)
+                add(SmallSize)
+                add(SmallSize)
+                add(XSmallSize, isAnchor = true)
+            }
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
new file mode 100644
index 0000000..27998ae
--- /dev/null
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -0,0 +1,357 @@
+/*
+ * 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.material3.carousel
+
+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 StrategyTest {
+
+    @Test
+    fun testStrategy_startAlignedStrategyShiftsEnd() {
+        val itemCount = 10
+        val carouselMainAxisSize = large + medium + small
+        val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
+        val defaultKeylineList = createStartAlignedKeylineList()
+
+        val strategy = Strategy.create(
+            carouselMainAxisSize = large + medium + small,
+            keylineList = defaultKeylineList
+        )
+
+        assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
+            .isEqualTo(defaultKeylineList)
+
+        val lastEndStepOffsets = arrayOf(-2.5f, 10f, 50f, 130f, 182.5f)
+        val lastEndStepUnadjustedOffsets = arrayOf(
+            130f - 300f,
+            130f - 200f,
+            130f - 100f,
+            130f,
+            130f + 100f
+        )
+        strategy.getKeylineListForScrollOffset(
+            maxScrollOffset,
+            maxScrollOffset,
+        ).forEachIndexed { i, k ->
+            assertThat(k.offset).isEqualTo(lastEndStepOffsets[i])
+            assertThat(k.unadjustedOffset).isEqualTo(lastEndStepUnadjustedOffsets[i])
+        }
+    }
+
+    @Test
+    fun testStrategy_centerAlignedShiftsStart() {
+        val itemCount = 12
+        val carouselMainAxisSize = (small * 2) + medium + (large * 2) + medium + (small * 2)
+        val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
+        val defaultKeylines = createCenterAlignedKeylineList()
+
+        val strategy = Strategy.create(
+            carouselMainAxisSize = carouselMainAxisSize,
+            keylineList = defaultKeylines
+        )
+
+        val startSteps = listOf(
+            // default step - [xs | s s m l l m s s | xs]
+            createCenterAlignedKeylineList(),
+            // step 1 - [xs | s m l l m s s s | xs]
+            keylineListOf(carouselMainAxisSize, 3, (small * 1 + medium + (large / 2))) {
+                add(xSmall, isAnchor = true)
+                add(small)
+                add(medium)
+                add(large)
+                add(large)
+                add(medium)
+                add(small)
+                add(small)
+                add(small)
+                add(xSmall, isAnchor = true)
+            },
+            // step 2 - [xs | m l l m s s s s | xs]
+            keylineListOf(carouselMainAxisSize, 2, medium + (large / 2)) {
+                add(xSmall, isAnchor = true)
+                add(medium)
+                add(large)
+                add(large)
+                add(medium)
+                add(small)
+                add(small)
+                add(small)
+                add(small)
+                add(xSmall, isAnchor = true)
+            },
+            // step 3 - [xs | l l m m s s s s | xs]
+            keylineListOf(carouselMainAxisSize, 1, large / 2) {
+                add(xSmall, isAnchor = true)
+                add(large)
+                add(large)
+                add(medium)
+                add(medium)
+                add(small)
+                add(small)
+                add(small)
+                add(small)
+                add(xSmall, isAnchor = true)
+            }
+        )
+
+        val shiftedStart = strategy.getKeylineListForScrollOffset(
+            0f,
+            maxScrollOffset,
+        )
+        assertThat(shiftedStart).isEqualTo(startSteps.last())
+        // Make sure the last shift towards start places the first focal item against the start of
+        // the carousel container.
+        assertThat(shiftedStart.firstFocal.offset - (shiftedStart.firstFocal.size / 2))
+            .isEqualTo(0)
+
+        // Test all shift steps start by manually calculating the scroll offset at each step and
+        // making sure getKeylineForScrollOffset returns the correct keyline list
+        val totalShiftStart =
+            startSteps.last().first().unadjustedOffset - startSteps.first().first().unadjustedOffset
+        val startStepsScrollOffsets = listOf(
+            totalShiftStart, // default step
+            totalShiftStart - (startSteps[1].first().unadjustedOffset -
+                startSteps[0].first().unadjustedOffset),
+            totalShiftStart - (startSteps[2].first().unadjustedOffset -
+                startSteps[0].first().unadjustedOffset),
+            totalShiftStart - (startSteps[3].first().unadjustedOffset -
+                startSteps[0].first().unadjustedOffset), // all the way start
+        )
+
+        startStepsScrollOffsets.forEachIndexed { i, scroll ->
+            val keylineList =
+                strategy.getKeylineListForScrollOffset(
+                    scroll,
+                    maxScrollOffset,
+                )
+            keylineList.forEachIndexed { j, keyline ->
+                assertEqualWithFloatTolerance(0.01f, keyline, startSteps[i][j])
+            }
+        }
+    }
+
+    @Test
+    fun testStrategy_centerAlignedShiftsEnd() {
+        val itemCount = 12
+        val carouselMainAxisSize = (small * 2) + medium + (large * 2) + medium + (small * 2)
+        val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
+        val defaultKeylines = createCenterAlignedKeylineList()
+
+        val strategy = Strategy.create(
+            carouselMainAxisSize = carouselMainAxisSize,
+            keylineList = defaultKeylines
+        )
+
+        val endSteps = listOf(
+            // default step
+            createCenterAlignedKeylineList(),
+            // step 1: Move a small item from after focal to beginning of focal
+            keylineListOf(carouselMainAxisSize, 5, (small * 3) + medium + (large / 2)) {
+                add(xSmall, isAnchor = true)
+                add(small)
+                add(small)
+                add(small)
+                add(medium)
+                add(large)
+                add(large)
+                add(medium)
+                add(small)
+                add(xSmall, isAnchor = true)
+            },
+            // step 2: Move another small from after focal to beginning of focal
+            keylineListOf(carouselMainAxisSize, 6, (small * 4) + medium + (large / 2)) {
+                add(xSmall, isAnchor = true)
+                add(small)
+                add(small)
+                add(small)
+                add(small)
+                add(medium)
+                add(large)
+                add(large)
+                add(medium)
+                add(xSmall, isAnchor = true)
+            },
+
+            // step 3: Move medium from after focal to beginning of focal
+            keylineListOf(carouselMainAxisSize, 7, (small * 4) + (medium * 2) + (large / 2)) {
+                add(xSmall, isAnchor = true)
+                add(small)
+                add(small)
+                add(small)
+                add(small)
+                add(medium)
+                add(medium)
+                add(large)
+                add(large)
+                add(xSmall, isAnchor = true)
+            }
+        )
+
+        val shiftedEnd = strategy.getKeylineListForScrollOffset(
+            maxScrollOffset,
+            maxScrollOffset,
+        )
+        assertThat(shiftedEnd).isEqualTo(endSteps.last())
+        // Make sure the last shift towards end places the last focal item against the end of the
+        // carousel container.
+        assertThat(shiftedEnd.lastFocal.offset + (shiftedEnd.lastFocal.size / 2))
+            .isEqualTo(carouselMainAxisSize)
+
+        val totalShiftEnd =
+            endSteps.first().last().unadjustedOffset - endSteps.last().last().unadjustedOffset
+        val endStepsScrollOffsets = listOf(
+            maxScrollOffset - totalShiftEnd, // default step
+            maxScrollOffset - totalShiftEnd - (endSteps[1].last().unadjustedOffset -
+                endSteps[0].last().unadjustedOffset),
+            maxScrollOffset - totalShiftEnd - (endSteps[2].last().unadjustedOffset -
+                endSteps[0].last().unadjustedOffset),
+            maxScrollOffset - totalShiftEnd - (endSteps[3].last().unadjustedOffset -
+                endSteps[0].last().unadjustedOffset), // all the way end
+        )
+
+        // Test exact scroll offset returns the correct step
+        endStepsScrollOffsets.forEachIndexed { i, scroll ->
+            val keylineList = strategy.getKeylineListForScrollOffset(scroll, maxScrollOffset)
+            keylineList.forEachIndexed { j, keyline ->
+                assertEqualWithFloatTolerance(0.01f, keyline, endSteps[i][j])
+            }
+        }
+
+        // Test non-exact scroll offset rounds to the correct step
+        val almostToStepOneOffset = endStepsScrollOffsets[0] +
+            ((endStepsScrollOffsets[1] - endStepsScrollOffsets[0]) * .75f)
+        assertThat(
+            strategy.getKeylineListForScrollOffset(
+                almostToStepOneOffset,
+                maxScrollOffset,
+                roundToNearestStep = true
+            )
+        )
+            .isEqualTo(endSteps[1])
+        val halfWayToStepTwo =
+            endStepsScrollOffsets[1] + ((endStepsScrollOffsets[2] - endStepsScrollOffsets[1]) * .5f)
+        assertThat(
+            strategy.getKeylineListForScrollOffset(
+                halfWayToStepTwo,
+                maxScrollOffset,
+                roundToNearestStep = true
+            )
+        )
+            .isEqualTo(endSteps[2])
+        val justPastStepTwo =
+            endStepsScrollOffsets[2] + ((endStepsScrollOffsets[3] - endStepsScrollOffsets[2]) * .1f)
+        assertThat(strategy.getKeylineListForScrollOffset(
+            justPastStepTwo,
+            maxScrollOffset,
+            roundToNearestStep = true
+        ))
+            .isEqualTo(endSteps[2])
+    }
+
+    @Test
+    fun testKeylineListLerp() {
+        val carouselMainAxisSize = large + medium + small
+        val from = keylineListOf(carouselMainAxisSize, 1, large / 2) {
+            add(xSmall, isAnchor = true)
+            add(large)
+            add(medium)
+            add(small)
+            add(xSmall, isAnchor = true)
+        }
+        val to = keylineListOf(carouselMainAxisSize, 2, small + (large / 2)) {
+            add(xSmall, isAnchor = true)
+            add(small)
+            add(large)
+            add(medium)
+            add(xSmall, isAnchor = true)
+        }
+
+        // Create the expected interpolated KeylineList by using the KeylineList class' constructor
+        // directly. Otherwise, keylineListOf will set offsets and unadjusted offsets based on the
+        // pivot offset and will differ than the directly interpolated output of lerp.
+        val half = KeylineList(
+            listOf(
+                Keyline(xSmall, -2.5f, -90f, false, true, false, 0f),
+                Keyline(60f, 30f, 10f, false, false, false, 0f),
+                Keyline(80f, 100f, 110f, true, false, true, 0f),
+                Keyline(40f, 160f, 210f, false, false, false, 0f),
+                Keyline(xSmall, 182.5f, 310f, false, true, false, 0f)
+            )
+        )
+
+        assertThat(lerp(from, to, 0f)).isEqualTo(from)
+        assertThat(lerp(from, to, 1f)).isEqualTo(to)
+        assertThat(lerp(from, to, .5f)).isEqualTo(half)
+    }
+
+    private fun assertEqualWithFloatTolerance(
+        tolerance: Float,
+        actual: Keyline,
+        expected: Keyline
+    ) {
+        assertThat(actual.size)
+            .isWithin(tolerance).of(expected.size)
+        assertThat(actual.offset)
+            .isWithin(tolerance).of(expected.offset)
+        assertThat(actual.unadjustedOffset)
+            .isWithin(0.01f).of(expected.unadjustedOffset)
+        assertThat(actual.isFocal).isEqualTo(expected.isFocal)
+        assertThat(actual.isAnchor).isEqualTo(expected.isAnchor)
+        assertThat(actual.isPivot).isEqualTo(expected.isPivot)
+        assertThat(actual.cutoff)
+            .isWithin(tolerance).of(expected.cutoff)
+    }
+
+    companion object {
+        val large = 100f
+        val small = 20f
+        val medium = (large + small) / 2
+        val xSmall = 5f
+
+        private fun createCenterAlignedKeylineList(): KeylineList {
+            // [xs | s s m l l m s s | xs]
+            val carouselMainAxisSize = (small * 2) + medium + (large * 2) + medium + (small * 2)
+            return keylineListOf(carouselMainAxisSize, CarouselAlignment.Center) {
+                add(xSmall, isAnchor = true)
+                add(small)
+                add(small)
+                add(medium)
+                add(large)
+                add(large)
+                add(medium)
+                add(small)
+                add(small)
+                add(xSmall, isAnchor = true)
+            }
+        }
+
+        private fun createStartAlignedKeylineList(): KeylineList {
+            // [xs | l m s | xs]
+            return keylineListOf(large + medium + small, CarouselAlignment.Start) {
+                add(xSmall, isAnchor = true)
+                add(large)
+                add(medium)
+                add(small)
+                add(xSmall, isAnchor = true)
+            }
+        }
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 1e7593f..0aef860 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -577,7 +577,7 @@
         contentColor: Color = LocalContentColor.current,
         disabledContainerColor: Color = Color.Unspecified,
         disabledContentColor: Color =
-            LocalContentColor.current.copy(alpha = IconButtonTokens.DisabledIconOpacity)
+            contentColor.copy(alpha = IconButtonTokens.DisabledIconOpacity)
     ): IconButtonColors = MaterialTheme.colorScheme.defaultIconButtonColors.copy(
         containerColor = containerColor,
         contentColor = contentColor,
@@ -588,14 +588,17 @@
     internal val ColorScheme.defaultIconButtonColors: IconButtonColors
         @Composable
         get() {
-            return defaultIconButtonColorsCached ?: IconButtonColors(
-                containerColor = Color.Transparent,
-                contentColor = LocalContentColor.current,
-                disabledContainerColor = Color.Transparent,
-                disabledContentColor =
-                LocalContentColor.current.copy(alpha = IconButtonTokens.DisabledIconOpacity)
-            ).also {
-                defaultIconButtonColorsCached = it
+            return defaultIconButtonColorsCached ?: run {
+                val localContentColor = LocalContentColor.current
+                IconButtonColors(
+                    containerColor = Color.Transparent,
+                    contentColor = localContentColor,
+                    disabledContainerColor = Color.Transparent,
+                    disabledContentColor =
+                    localContentColor.copy(alpha = IconButtonTokens.DisabledIconOpacity)
+                ).also {
+                    defaultIconButtonColorsCached = it
+                }
             }
         }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt
new file mode 100644
index 0000000..472a0eb
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt
@@ -0,0 +1,470 @@
+/*
+ * 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.material3.carousel
+
+import androidx.compose.ui.util.fastFirstOrNull
+
+/**
+ * A structure that is fixed at a specific [offset] along a scrolling axis and
+ * defines properties of an item when its center is located at [offset].
+ *
+ * [Keyline] is the primary structure of any carousel. When multiple keylines are placed along a
+ * carousel's axis and an item is scrolled, that item will always be between two keylines. The
+ * item's distance between its two surrounding keylines can be used as a fraction to create an
+ * interpolated keyline that the item uses to set its size and translation.
+ *
+ * @param size the size an item should be in pixels when its center is at [offset]
+ * @param offset the location of the keyline along the scrolling axis and where the center of an
+ * item should be (usually translated to) when it is at [unadjustedOffset] in the end-to-end
+ * scrolling model
+ * @param unadjustedOffset the location of:445
+ * the keyline in the end-to-end scrolling model (when all
+ * items are laid out with their full size and placed end-to-end)
+ * @param isFocal whether an item at this keyline is focal or fully "viewable"
+ * @param isAnchor true if this keyline is able to be shifted within a list of keylines
+ * @param isPivot true if this is the keyline that was used to calculate all other keyline offsets
+ * and unadjusted offsets in a list
+ * @param cutoff the amount this item bleeds beyond the bounds of the container - 0 if the item
+ * is fully in-bounds or fully out-of-bounds
+ */
+internal data class Keyline(
+    val size: Float,
+    val offset: Float,
+    val unadjustedOffset: Float,
+    val isFocal: Boolean,
+    val isAnchor: Boolean,
+    val isPivot: Boolean,
+    val cutoff: Float,
+)
+
+/**
+ * A [List] of [Keyline]s with additional functionality specific to carousel.
+ *
+ * Note that [KeylineList]'s constructor should only be used when creating an interpolated
+ * KeylineList. If creating a new KeylineList - for a strategy or shifted step - prefer using the
+ * [keylineListOf] method which will handle setting all offsets and unadjusted offsets based on
+ * a pivot keyline.
+ */
+internal class KeylineList internal constructor(
+    keylines: List<Keyline>
+) : List<Keyline> by keylines {
+
+    /**
+     * Returns the index of the pivot keyline used to calculate all other keyline offsets and
+     * unadjusted offsets.
+     */
+    val pivotIndex: Int = indexOfFirst { it.isPivot }
+
+    /** Returns the keyline used to calculate all other keyline offsets and unadjusted offsets. */
+    val pivot: Keyline = get(pivotIndex)
+
+    /**
+     * Returns the index of the first non-anchor keyline or -1 if the list does not contain a
+     * non-anchor keyline.
+     */
+    val firstNonAnchorIndex: Int = indexOfFirst { !it.isAnchor }
+
+    /**
+     * Returns the first non-anchor [Keyline].
+     * @throws [NoSuchElementException] if there are no non-anchor keylines.
+     */
+    val firstNonAnchor: Keyline = get(firstNonAnchorIndex)
+
+    /**
+     * Returns the index of the last non-anchor keyline or -1 if the list does not contain a
+     * non-anchor keyline.
+     */
+    val lastNonAnchorIndex: Int = indexOfLast { !it.isAnchor }
+
+    /**
+     * Returns the last non-anchor [Keyline].
+     * @throws [NoSuchElementException] if there are no non-anchor keylines.
+     */
+    val lastNonAnchor = get(lastNonAnchorIndex)
+
+    /**
+     * Returns the index of the first focal keyline or -1 if the list does not contain a
+     * focal keyline.
+     */
+    val firstFocalIndex = indexOfFirst { it.isFocal }
+
+    /**
+     * Returns the first focal [Keyline].
+     * @throws [NoSuchElementException] if there are no focal keylines.
+     */
+    val firstFocal: Keyline = getOrNull(firstFocalIndex)
+        ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
+
+    /**
+     * Returns the index of the last focal keyline or -1 if the list does not contain a
+     * focal keyline.
+     */
+    val lastFocalIndex: Int = indexOfLast { it.isFocal }
+
+    /**
+     * Returns the last focal [Keyline].
+     * @throws [NoSuchElementException] if there are no focal keylines.
+     */
+    val lastFocal = getOrNull(lastFocalIndex)
+        ?: throw NoSuchElementException("All KeylineLists must have at least one focal keyline")
+
+    /**
+     * Returns true if the first focal item's left/top is within the visible bounds of the container
+     * and is the first non-anchor keyline.
+     *
+     * When this is true, it means the focal range cannot be shifted left/top or is shifted as
+     * far left/top as possible. When this is false, there are keylines that can be swapped to
+     * shift the first focal item closer to the left/top of the container while still remaining
+     * visible.
+     */
+    fun isFirstFocalItemAtStartOfContainer(): Boolean {
+        val firstFocalLeft = firstFocal.offset - (firstFocal.size / 2)
+        return firstFocalLeft >= 0 && firstFocal == firstNonAnchor
+    }
+
+    /**
+     * Returns true if the last focal item's right/bottom is within the visible bounds of the
+     * container and is the last non-anchor keyline.
+     *
+     * When this is true, it means the focal range cannot be shifted right/bottom or is shifted as
+     * far right/bottom as possible. When this is false, there are keylines that can be swapped to
+     * shift the last focal item closer to the right/bottom of the container while still remaining
+     * visible.
+     */
+    fun isLastFocalItemAtEndOfContainer(carouselMainAxisSize: Float): Boolean {
+        val lastFocalRight = lastFocal.offset + (lastFocal.size / 2)
+        return lastFocalRight <= carouselMainAxisSize && lastFocal == lastNonAnchor
+    }
+
+    /**
+     * Returns the index of the first keyline after the focal range where the keyline's size is
+     * equal to [size] or the last index if no keyline is found.
+     *
+     * This is useful when moving keylines from one side of the focal range to the other (shifting).
+     * Find an index on the other side of the focal range where after moving the keyline, the
+     * keyline list will retain its original visual balance.
+     */
+    fun firstIndexAfterFocalRangeWithSize(size: Float): Int {
+        val from = lastFocalIndex
+        val to = lastIndex
+        return (from..to).firstOrNull { i -> this[i].size == size } ?: lastIndex
+    }
+
+    /**
+     * Returns the index of the last keyline before the focal range where the keyline's size is
+     * equal to [size] or 0 if no keyline is found.
+     *
+     * This is useful when moving keylines from one side of the focal range to the other (shifting).
+     * Find an index on the other side of the focal range where after moving the keyline, the
+     * keyline list will retain its original visual balance.
+     */
+    fun lastIndexBeforeFocalRangeWithSize(size: Float): Int {
+        val from = firstFocalIndex - 1
+        val to = 0
+        return (from downTo to).firstOrNull { i -> this[i].size == size } ?: to
+    }
+
+    /**
+     * Returns the last [Keyline] with an unadjustedOffset that is less than [unadjustedOffset] or
+     * the first keyline if none is found.
+     */
+    fun getKeylineBefore(unadjustedOffset: Float): Keyline {
+        for (index in indices.reversed()) {
+            val k = get(index)
+            if (k.unadjustedOffset < unadjustedOffset) {
+                return k
+            }
+        }
+
+        return first()
+    }
+
+    /**
+     * Returns the first [Keyline] with an unadjustedOffset that is greater than
+     * [unadjustedOffset] or the last keyline if none is found.
+     */
+    fun getKeylineAfter(unadjustedOffset: Float): Keyline {
+        return fastFirstOrNull { it.unadjustedOffset >= unadjustedOffset } ?: last()
+    }
+}
+
+internal enum class CarouselAlignment {
+    Start, Center, End
+}
+
+/**
+ * Returns a [KeylineList] by aligning the focal range relative to the carousel container.
+ */
+internal fun keylineListOf(
+    carouselMainAxisSize: Float,
+    carouselAlignment: CarouselAlignment,
+    keylines: KeylineListScope.() -> Unit
+): KeylineList {
+    val keylineListScope = KeylineListScopeImpl()
+    keylines.invoke(keylineListScope)
+    return keylineListScope.createWithAlignment(carouselMainAxisSize, carouselAlignment)
+}
+
+/**
+ * Returns a [KeylineList] by using a single pivot keyline to calculate the offset and unadjusted
+ * offset of all keylines in the list.
+ */
+internal fun keylineListOf(
+    carouselMainAxisSize: Float,
+    pivotIndex: Int,
+    pivotOffset: Float,
+    keylines: KeylineListScope.() -> Unit
+): KeylineList {
+    val keylineListScope = KeylineListScopeImpl()
+    keylines.invoke(keylineListScope)
+    return keylineListScope.createWithPivot(carouselMainAxisSize, pivotIndex, pivotOffset)
+}
+
+/** Receiver scope for creating a [KeylineList] using [keylineListOf] */
+internal interface KeylineListScope {
+
+    /**
+     * Adds a keyline to the resulting [KeylineList].
+     *
+     * Note that keylines are added in the order they will appear.
+     *
+     * @param size the size of an item in pixels at this keyline
+     * @param isAnchor true if this keyline should not be shifted - usually the first and last fully
+     * off-screen keylines
+     */
+    fun add(size: Float, isAnchor: Boolean = false)
+}
+
+private class KeylineListScopeImpl : KeylineListScope {
+
+    private data class TmpKeyline(val size: Float, val isAnchor: Boolean)
+
+    private var firstFocalIndex: Int = -1
+    private var focalItemSize: Float = 0f
+    private var pivotIndex: Int = -1
+    private var pivotOffset: Float = 0f
+    private val tmpKeylines = mutableListOf<TmpKeyline>()
+    override fun add(size: Float, isAnchor: Boolean) {
+        tmpKeylines.add(TmpKeyline(size, isAnchor))
+        // Save the first "focal" item by looking for the first index of the largest item added
+        // to the list. The last focal item index will be found when `create` is called by starting
+        // from firstFocalIndex and incrementing the index until the next item's size does not
+        // equal focalItemSize.
+        if (size > focalItemSize) {
+            firstFocalIndex = tmpKeylines.lastIndex
+            focalItemSize = size
+        }
+    }
+
+    fun createWithPivot(
+        carouselMainAxisSize: Float,
+        pivotIndex: Int,
+        pivotOffset: Float
+    ): KeylineList {
+        val keylines = createKeylinesWithPivot(
+            pivotIndex,
+            pivotOffset,
+            firstFocalIndex,
+            findLastFocalIndex(),
+            itemMainAxisSize = focalItemSize,
+            carouselMainAxisSize = carouselMainAxisSize,
+            tmpKeylines
+        )
+        return KeylineList(keylines)
+    }
+
+    fun createWithAlignment(
+        carouselMainAxisSize: Float,
+        carouselAlignment: CarouselAlignment
+    ): KeylineList {
+        val lastFocalIndex = findLastFocalIndex()
+        val focalItemCount = lastFocalIndex - firstFocalIndex
+
+        pivotIndex = firstFocalIndex
+        pivotOffset = when (carouselAlignment) {
+            CarouselAlignment.Start -> focalItemSize / 2
+            CarouselAlignment.Center -> {
+                (carouselMainAxisSize / 2) - ((focalItemSize / 2) * focalItemCount)
+            }
+            CarouselAlignment.End -> carouselMainAxisSize - (focalItemSize / 2)
+        }
+
+        val keylines = createKeylinesWithPivot(
+            pivotIndex,
+            pivotOffset,
+            firstFocalIndex,
+            lastFocalIndex,
+            itemMainAxisSize = focalItemSize,
+            carouselMainAxisSize = carouselMainAxisSize,
+            tmpKeylines
+        )
+        return KeylineList(keylines)
+    }
+
+    private fun findLastFocalIndex(): Int {
+        // Find the last focal index. Start from the first focal index and walk up the indices
+        // while items remain the same size as the first focal item size - finding a contiguous
+        // range of indices where item size is equal to focalItemSize.
+        var lastFocalIndex = firstFocalIndex
+        while (lastFocalIndex < tmpKeylines.lastIndex &&
+            tmpKeylines[lastFocalIndex + 1].size == focalItemSize) {
+            lastFocalIndex ++
+        }
+        return lastFocalIndex
+    }
+
+    /**
+     * Converts a list of [TmpKeyline] to a list of [Keyline]s whose offset, unadjusted offset,
+     * and cutoff are calculated from a pivot.
+     *
+     * Pivoting is useful when aligning the entire arrangement relative to the scrolling container.
+     * When creating a keyline list with the first focal keyline aligned to the start of the
+     * container, use the first focal item as the pivot and set the pivot offset to where that first
+     * focal item's center should be placed (carouselStart + (item size / 2)). All keylines
+     * before and after the pivot will have their offset, unadjusted offset, and cutoff calculated
+     * based on the pivot offset. When shifting keylines and moving the carousel's alignment from
+     * start to end, use setPivot to align the last focal keyline to the end of the container.
+     *
+     * @param pivotIndex the index of the keyline from [tmpKeylines] that is used to align the
+     * entire arrangement
+     * @param pivotOffset the offset along the scrolling axis where the pivot keyline should
+     * be placed and where keylines before and after will have their offset, unadjustedOffset, and
+     * cutoff calculated from
+     * @param firstFocalIndex the index of the first focal item in the [tmpKeylines] list
+     * @param lastFocalIndex the index of the last focal item in the [tmpKeylines] list
+     * @param itemMainAxisSize the size of focal, or fully unmasked/clipped, items
+     * @param carouselMainAxisSize the size of the carousel container in the scrolling axis
+     */
+    private fun createKeylinesWithPivot(
+        pivotIndex: Int,
+        pivotOffset: Float,
+        firstFocalIndex: Int,
+        lastFocalIndex: Int,
+        itemMainAxisSize: Float,
+        carouselMainAxisSize: Float,
+        tmpKeylines: List<TmpKeyline>
+    ): List<Keyline> {
+        val pivot = tmpKeylines[pivotIndex]
+        val keylines = mutableListOf<Keyline>()
+
+        val pivotCutoff: Float = when {
+            isCutoffLeft(pivot.size, pivotOffset) -> pivotOffset - (pivot.size / 2)
+            isCutoffRight(
+                pivot.size,
+                pivotOffset,
+                carouselMainAxisSize
+            ) -> (pivotOffset + (pivot.size / 2)) - carouselMainAxisSize
+
+            else -> 0f
+        }
+        keylines.add(
+            // Add the pivot keyline first
+            Keyline(
+                size = pivot.size,
+                offset = pivotOffset,
+                unadjustedOffset = pivotOffset,
+                isFocal = pivotIndex in firstFocalIndex..lastFocalIndex,
+                isAnchor = pivot.isAnchor,
+                isPivot = true,
+                cutoff = pivotCutoff
+            )
+        )
+
+        // Convert all TmpKeylines before the pivot to Keylines by calculating their offset,
+        // unadjustedOffset, and cutoff and insert them at the beginning of the keyline list,
+        // maintaining the tmpKeyline list's original order.
+        var offset = pivotOffset - (itemMainAxisSize / 2)
+        var unadjustedOffset = pivotOffset - (itemMainAxisSize / 2)
+        (pivotIndex - 1 downTo 0).forEach { originalIndex ->
+            val tmp = tmpKeylines[originalIndex]
+            val tmpOffset = offset - (tmp.size / 2)
+            val tmpUnadjustedOffset = unadjustedOffset - (itemMainAxisSize / 2)
+            val cutoff = if (isCutoffLeft(tmp.size, tmpOffset)) tmpOffset - (tmp.size / 2) else 0f
+            keylines.add(0,
+                Keyline(
+                    size = tmp.size,
+                    offset = tmpOffset,
+                    unadjustedOffset = tmpUnadjustedOffset,
+                    isFocal = originalIndex in firstFocalIndex..lastFocalIndex,
+                    isAnchor = tmp.isAnchor,
+                    isPivot = false,
+                    cutoff = cutoff
+                )
+            )
+
+            offset -= tmp.size
+            unadjustedOffset -= itemMainAxisSize
+        }
+
+        // Convert all TmpKeylines after the pivot to Keylines by calculating their offset,
+        // unadjustedOffset, and cutoff and inserting them at the end of the keyline list,
+        // maintaining the tmpKeyline list's original order.
+        offset = pivotOffset + (itemMainAxisSize / 2)
+        unadjustedOffset = pivotOffset + (itemMainAxisSize / 2)
+        (pivotIndex + 1 until tmpKeylines.size).forEach { originalIndex ->
+            val tmp = tmpKeylines[originalIndex]
+            val tmpOffset = offset + (tmp.size / 2)
+            val tmpUnadjustedOffset = unadjustedOffset + (itemMainAxisSize / 2)
+            val cutoff = if (isCutoffRight(tmp.size, tmpOffset, carouselMainAxisSize)) {
+                (tmpOffset + (tmp.size / 2)) - carouselMainAxisSize
+            } else {
+                0f
+            }
+            keylines.add(
+                Keyline(
+                    size = tmp.size,
+                    offset = tmpOffset,
+                    unadjustedOffset = tmpUnadjustedOffset,
+                    isFocal = originalIndex in firstFocalIndex..lastFocalIndex,
+                    isAnchor = tmp.isAnchor,
+                    isPivot = false,
+                    cutoff = cutoff
+                )
+            )
+
+            offset += tmp.size
+            unadjustedOffset += itemMainAxisSize
+        }
+
+        return keylines
+    }
+
+    /**
+     * Returns whether an item of [size] whose center is at [offset] is straddling the carousel
+     * container's left/top.
+     *
+     * This method will return false if the item is either fully visible (its left/top edge
+     * comes after the container's left/top) or fully invisible (its right/bottom edge comes before
+     * the container's left/top).
+     */
+    private fun isCutoffLeft(size: Float, offset: Float): Boolean {
+        return offset - (size / 2) < 0f && offset + (size / 2) > 0f
+    }
+
+    /**
+     * Returns whether an item of [size] whose center is at [offset] is straddling the carousel
+     * container's right/bottom edge.
+     *
+     * This method will return false if the item is either fully visible (its right/bottom edge
+     * comes before the container's right/bottom) or fully invisible (its left/top edge comes
+     * after the container's right/bottom).
+     */
+    private fun isCutoffRight(size: Float, offset: Float, carouselMainAxisSize: Float): Boolean {
+        return offset - (size / 2) < carouselMainAxisSize &&
+            offset + (size / 2) > carouselMainAxisSize
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
new file mode 100644
index 0000000..269d0c5
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -0,0 +1,508 @@
+/*
+ * 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.material3.carousel
+
+import androidx.annotation.VisibleForTesting
+import androidx.collection.FloatList
+import androidx.collection.mutableFloatListOf
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMapIndexed
+import androidx.compose.ui.util.lerp
+import kotlin.math.roundToInt
+
+/**
+ * An interface which provides [Strategy] instances to a scrollable component.
+ *
+ * [StrategyProvider.createStrategy] will be called any time properties which affect a carousel's
+ * arrangement change. It is the implementation's responsibility to create an arrangement for the
+ * given parameters and return a [Strategy] by calling [Strategy.create].
+ */
+internal interface StrategyProvider {
+    /**
+     * Create and return a new [Strategy] for the given carousel size.
+     *
+     * TODO: Add additional parameters like alignment and item count.
+     *
+     * @param density The [Density] object that provides pixel density information of the device
+     * @param carouselMainAxisSize the size of the carousel in the main axis in pixels
+     */
+    fun createStrategy(
+        density: Density,
+        carouselMainAxisSize: Float,
+    ): Strategy
+}
+
+/**
+ * A class which supplies carousel with the appropriate [KeylineList] for any given scroll offset.
+ *
+ * All items in a carousel need the opportunity to pass through the focal keyline range. Depending
+ * on where the focal range is located within the scrolling container, some items, like those at
+ * the beginning or end of the list, might not reach the focal range. To account for this,
+ * [Strategy] manages shifting the focal keylines to the beginning of the list when scrolled an
+ * offset of 0 and the end of the list when scrolled to the list's max offset. [StrategyProvider]
+ * needs only to create a "default" [KeylineList] (where keylines should be placed when scrolling
+ * in the middle of the list) and call [Strategy.create] to have [Strategy] generate the steps
+ * needed to move the focal range to the start and end of the scroll container. When scrolling, the
+ * scrollable component can access the correct [KeylineList] for any given offset using
+ * [getKeylineListForScrollOffset].
+ *
+ * @param defaultKeylines the [KeylineList] used when anywhere in the center of the list
+ * @param startKeylineSteps a list of [KeylineList]s that will be moved through when approaching
+ * the beginning of the list
+ * @param endKeylineSteps a list o [KeylineList]s that will be moved through when appraoching
+ * the end of the list
+ * @param startShiftDistance the scroll distance it should take to move through all the steps in
+ * [startKeylineSteps]
+ * @param endShiftDistance the scroll distance it should take to move through all the steps in the
+ * [endKeylineSteps]
+ * @param startShiftPoints a list of floats between 0-1 that define the percentage of shift distance
+ * at which the start keyline step at the corresponding index should be used
+ * @param endShiftPoints a list of floats between 0-1 that define the percentage of shift distance
+ * at which the end keyline step at the corresponding index should be used
+ */
+internal class Strategy private constructor(
+    private val defaultKeylines: KeylineList,
+    private val startKeylineSteps: List<KeylineList>,
+    private val endKeylineSteps: List<KeylineList>,
+    private val startShiftDistance: Float,
+    private val endShiftDistance: Float,
+    private val startShiftPoints: FloatList,
+    private val endShiftPoints: FloatList
+) {
+
+    /**
+     * Returns the [KeylineList] that should be used for the current [scrollOffset].
+     *
+     * @param scrollOffset the current scroll offset of the scrollable component
+     * @param maxScrollOffset the maximum scroll offset
+     * @param roundToNearestStep true if the KeylineList returned should be a complete shift step
+     */
+    internal fun getKeylineListForScrollOffset(
+        scrollOffset: Float,
+        maxScrollOffset: Float,
+        roundToNearestStep: Boolean = false
+    ): KeylineList {
+        val startShiftOffset = startShiftDistance
+        val endShiftOffset = maxScrollOffset - endShiftDistance
+
+        // If we're not within either shift range, return the default keylines
+        if (scrollOffset in startShiftOffset..endShiftOffset) {
+            return defaultKeylines
+        }
+
+        var interpolation = lerp(
+            outputMin = 1f,
+            outputMax = 0f,
+            inputMin = 0f,
+            inputMax = startShiftOffset,
+            value = scrollOffset
+        )
+        var shiftPoints = startShiftPoints
+        var steps = startKeylineSteps
+
+        if (scrollOffset > endShiftOffset) {
+            interpolation = lerp(
+                outputMin = 0f,
+                outputMax = 1f,
+                inputMin = endShiftOffset,
+                inputMax = maxScrollOffset,
+                value = scrollOffset
+            )
+            shiftPoints = endShiftPoints
+            steps = endKeylineSteps
+        }
+
+        val shiftPointRange = getShiftPointRange(
+            steps.size,
+            shiftPoints,
+            interpolation
+        )
+
+        if (roundToNearestStep) {
+            val roundedStepIndex = if (shiftPointRange.steppedInterpolation.roundToInt() == 0) {
+                shiftPointRange.fromStepIndex
+            } else {
+                shiftPointRange.toStepIndex
+            }
+            return steps[roundedStepIndex]
+        }
+
+        return lerp(
+            steps[shiftPointRange.fromStepIndex],
+            steps[shiftPointRange.toStepIndex],
+            shiftPointRange.steppedInterpolation
+        )
+    }
+
+    companion object {
+
+        /**
+         * Creates a new [Strategy] based on a default [keylineList].
+         *
+         * The [keylineList] passed to this method will be the keylines used when the carousel is
+         * scrolled anywhere in the middle of the list (not the beginning or end). From these
+         * default keylines, additional [KeylineList]s will be created which move the focal range
+         * to the beginning of the carousel container when scrolled to the beginning of the list and
+         * the end of the container when scrolled to the end of the list.
+         *
+         * @param carouselMainAxisSize the size of the carousel container in scrolling axis
+         * @param keylineList the default keylines that will be used to create the strategy
+         */
+        internal fun create(
+            /** The size of the carousel in the main axis. */
+            carouselMainAxisSize: Float,
+            /** The keylines along the main axis */
+            keylineList: KeylineList
+        ): Strategy {
+
+            val startKeylineSteps = getStartKeylineSteps(keylineList, carouselMainAxisSize)
+            val endKeylineSteps =
+                getEndKeylineSteps(keylineList, carouselMainAxisSize)
+
+            // TODO: Update this to use the first/last focal keylines to calculate shift?
+            val startShiftDistance = startKeylineSteps.last().first().unadjustedOffset -
+                keylineList.first().unadjustedOffset
+            val endShiftDistance = keylineList.last().unadjustedOffset -
+                endKeylineSteps.last().last().unadjustedOffset
+
+            return Strategy(
+                defaultKeylines = keylineList,
+                startKeylineSteps = startKeylineSteps,
+                endKeylineSteps = endKeylineSteps,
+                startShiftDistance = startShiftDistance,
+                endShiftDistance = endShiftDistance,
+                startShiftPoints = getStepInterpolationPoints(
+                    startShiftDistance,
+                    startKeylineSteps,
+                    true
+                ),
+                endShiftPoints = getStepInterpolationPoints(
+                    endShiftDistance,
+                    endKeylineSteps,
+                    false
+                )
+            )
+        }
+
+        /**
+         * Generates discreet steps which move the focal range from its original position until
+         * it reaches the start of the carousel container.
+         *
+         * Each step can only move the focal range by one keyline at a time to ensure every
+         * item in the list passes through the focal range. Each step removes the keyline at the
+         * start of the container and re-inserts it after the focal range in an order that retains
+         * visual balance. This is repeated until the first focal keyline is at the start of the
+         * container. Re-inserting keylines after the focal range in a balanced way is done by
+         * looking at the size of they keyline next to the keyline that is being re-positioned
+         * and finding a match on the other side of the focal range.
+         *
+         * The first state in the returned list is always the default [KeylineList] while
+         * the last state will be the start state or the state that has the focal range at the
+         * beginning of the carousel.
+         */
+        private fun getStartKeylineSteps(
+            defaultKeylines: KeylineList,
+            carouselMainAxisSize: Float
+        ): List<KeylineList> {
+            val steps: MutableList<KeylineList> = mutableListOf()
+            steps.add(defaultKeylines)
+
+            if (defaultKeylines.isFirstFocalItemAtStartOfContainer()) {
+                return steps
+            }
+
+            val startIndex = defaultKeylines.firstNonAnchorIndex
+            val endIndex = defaultKeylines.firstFocalIndex
+            val numberOfSteps = endIndex - startIndex
+
+            // If there are no steps but we need to account for a cutoff, create a
+            // list of keylines shifted for the cutoff.
+            if (numberOfSteps <= 0 && defaultKeylines.firstFocal.cutoff > 0) {
+                steps.add(
+                    moveKeylineAndCreateShiftedKeylineList(
+                        from = defaultKeylines,
+                        srcIndex = 0,
+                        dstIndex = 0,
+                        carouselMainAxisSize = carouselMainAxisSize
+                    )
+                )
+                return steps
+            }
+
+            var i = 0
+            while (i < numberOfSteps) {
+                val prevStep = steps.last()
+                val originalItemIndex = startIndex + i
+                var dstIndex = defaultKeylines.lastIndex
+                if (originalItemIndex > 0) {
+                    val originalNeighborBeforeSize = defaultKeylines[originalItemIndex - 1].size
+                    dstIndex = prevStep.firstIndexAfterFocalRangeWithSize(
+                        originalNeighborBeforeSize
+                    ) - 1
+                }
+
+                steps.add(
+                    moveKeylineAndCreateShiftedKeylineList(
+                        from = prevStep,
+                        srcIndex = defaultKeylines.firstNonAnchorIndex,
+                        dstIndex = dstIndex,
+                        carouselMainAxisSize = carouselMainAxisSize
+                    )
+                )
+                i++
+            }
+
+            return steps
+        }
+
+        /**
+         * Generates discreet steps which move the focal range from its original position until
+         * it reaches the end of the carousel container.
+         *
+         * Each step can only move the focal range by one keyline at a time to ensure every
+         * item in the list passes through the focal range. Each step removes the keyline at the
+         * end of the container and re-inserts it before the focal range in an order that retains
+         * visual balance. This is repeated until the last focal keyline is at the start of the
+         * container. Re-inserting keylines before the focal range in a balanced way is done by
+         * looking at the size of they keyline next to the keyline that is being re-positioned
+         * and finding a match on the other side of the focal range.
+         *
+         * The first state in the returned list is always the default [KeylineList] while
+         * the last state will be the end state or the state that has the focal range at the
+         * end of the carousel.
+         */
+        private fun getEndKeylineSteps(
+            defaultKeylines: KeylineList,
+            carouselMainAxisSize: Float
+        ): List<KeylineList> {
+            val steps: MutableList<KeylineList> = mutableListOf()
+            steps.add(defaultKeylines)
+
+            if (defaultKeylines.isLastFocalItemAtEndOfContainer(carouselMainAxisSize)) {
+                return steps
+            }
+
+            val startIndex = defaultKeylines.lastFocalIndex
+            val endIndex = defaultKeylines.lastNonAnchorIndex
+            val numberOfSteps = endIndex - startIndex
+
+            // If there are no steps but we need to account for a cutoff, create a
+            // list of keylines shifted for the cutoff.
+            if (numberOfSteps <= 0 && defaultKeylines.lastFocal.cutoff > 0) {
+                steps.add(
+                    moveKeylineAndCreateShiftedKeylineList(
+                        from = defaultKeylines,
+                        srcIndex = 0,
+                        dstIndex = 0,
+                        carouselMainAxisSize = carouselMainAxisSize
+                    )
+                )
+                return steps
+            }
+
+            var i = 0
+            while (i < numberOfSteps) {
+                val prevStep = steps.last()
+                val originalItemIndex = endIndex - i
+                var dstIndex = 0
+
+                if (originalItemIndex < defaultKeylines.lastIndex) {
+                    val originalNeighborAfterSize = defaultKeylines[originalItemIndex + 1].size
+                    dstIndex = prevStep.lastIndexBeforeFocalRangeWithSize(
+                        originalNeighborAfterSize
+                    ) + 1
+                }
+
+                val keylines = moveKeylineAndCreateShiftedKeylineList(
+                    from = prevStep,
+                    srcIndex = defaultKeylines.lastNonAnchorIndex,
+                    dstIndex = dstIndex,
+                    carouselMainAxisSize = carouselMainAxisSize
+                )
+                steps.add(keylines)
+                i++
+            }
+
+            return steps
+        }
+
+        /**
+         * Returns a new [KeylineList] where the keyline at [srcIndex] is moved to [dstIndex] and
+         * with updated pivot and offsets that reflect any change in focal shift.
+         */
+        private fun moveKeylineAndCreateShiftedKeylineList(
+            from: KeylineList,
+            srcIndex: Int,
+            dstIndex: Int,
+            carouselMainAxisSize: Float
+        ): KeylineList {
+            // -1 if the pivot is shifting left/top, 1 if shifting right/bottom
+            val pivotDir = if (srcIndex > dstIndex) 1 else -1
+            val pivotDelta = from[srcIndex].size * pivotDir
+            val newPivotIndex = from.pivotIndex + pivotDir
+            val newPivotOffset = from.pivot.offset + pivotDelta
+            return keylineListOf(carouselMainAxisSize, newPivotIndex, newPivotOffset) {
+                from.toMutableList()
+                    .move(srcIndex, dstIndex)
+                    .fastForEach { k -> add(k.size, k.isAnchor) }
+            }
+        }
+
+        /**
+         * Creates and returns a list of float values containing points between 0 and 1 that
+         * represent interpolation values for when the [KeylineList] at the corresponding index in
+         * [steps] should be visible.
+         *
+         * For example, if [steps] has a size of 4, this method will return an array of 4 float
+         * values that could look like [0, .33, .66, 1]. When interpolating through a list of
+         * [KeylineList]s, an interpolation value will be between 0-1. This interpolation will be
+         * used to find the range it falls within from this method's returned value. If
+         * interpolation is .25, that would fall between the 0 and .33, the 0th and 1st indices
+         * of the float array. Meaning the 0th and 1st items from [steps] should be the current
+         * [KeylineList]s being interpolated. This is an example with equally distributed values
+         * but these values will typically be unequally distributed since their size depends on
+         * the distance keylines shift between each step.
+         *
+         * @see [lerp] for more details on how interpolation points are used
+         * @see [getKeylineListForScrollOffset] for more details on how interpolation points
+         * are used
+         *
+         * @param totalShiftDistance the total distance keylines will shift between the first and
+         * last [KeylineList] of [steps]
+         * @param steps the steps to find interpolation points for
+         * @param isShiftingLeft true if this method should find interpolation points for shifting
+         * keylines to the left/top of a carousel, false if this method should find interpolation
+         * points for shifting keylines to the right/bottom of a carousel
+         * @return a list of floats, equal in size to [steps] that contains points between 0-1
+         * that align with when a [KeylineList] from [steps should be shown for a 0-1
+         * interpolation value
+         */
+        private fun getStepInterpolationPoints(
+            totalShiftDistance: Float,
+            steps: List<KeylineList>,
+            isShiftingLeft: Boolean
+        ): FloatList {
+            val points = mutableFloatListOf(0f)
+            if (totalShiftDistance == 0f) {
+                return points
+            }
+
+            (1 until steps.size).map { i ->
+                val prevKeylines = steps[i - 1]
+                val currKeylines = steps[i]
+                val distanceShifted = if (isShiftingLeft) {
+                    currKeylines.first().unadjustedOffset - prevKeylines.first().unadjustedOffset
+                } else {
+                    prevKeylines.last().unadjustedOffset - currKeylines.last().unadjustedOffset
+                }
+                val stepPercentage = distanceShifted / totalShiftDistance
+                val point = if (i == steps.lastIndex) 1f else points[i - 1] + stepPercentage
+                points.add(point)
+            }
+            return points
+        }
+
+        private data class ShiftPointRange(
+            val fromStepIndex: Int,
+            val toStepIndex: Int,
+            val steppedInterpolation: Float
+        )
+
+        private fun getShiftPointRange(
+            stepsCount: Int,
+            shiftPoint: FloatList,
+            interpolation: Float
+        ): ShiftPointRange {
+            var lowerBounds = shiftPoint[0]
+            (1 until stepsCount).forEach { i ->
+                val upperBounds = shiftPoint[i]
+                if (interpolation <= upperBounds) {
+                    return ShiftPointRange(
+                        fromStepIndex = i - 1,
+                        toStepIndex = i,
+                        steppedInterpolation = lerp(0f, 1f, lowerBounds, upperBounds, interpolation)
+                    )
+                }
+                lowerBounds = upperBounds
+            }
+            return ShiftPointRange(
+                fromStepIndex = 0,
+                toStepIndex = 0,
+                steppedInterpolation = 0f)
+        }
+
+        private fun MutableList<Keyline>.move(srcIndex: Int, dstIndex: Int): MutableList<Keyline> {
+            val keyline = get(srcIndex)
+            removeAt(srcIndex)
+            add(dstIndex, keyline)
+            return this
+        }
+    }
+}
+
+/**
+ * Returns an interpolated [Keyline] whose values are all interpolated based on [fraction]
+ * between the [start] and [end] keylines.
+ */
+@VisibleForTesting
+internal fun lerp(start: Keyline, end: Keyline, fraction: Float): Keyline {
+    return Keyline(
+        size = lerp(start.size, end.size, fraction),
+        offset = lerp(start.offset, end.offset, fraction),
+        unadjustedOffset = lerp(start.unadjustedOffset, end.unadjustedOffset, fraction),
+        isFocal = if (fraction < .5f) start.isFocal else end.isFocal,
+        isAnchor = if (fraction < .5f) start.isAnchor else end.isAnchor,
+        isPivot = if (fraction < .5f) start.isPivot else end.isPivot,
+        cutoff = lerp(start.cutoff, end.cutoff, fraction)
+    )
+}
+
+/**
+ * Returns an interpolated KeylineList between [from] and [to].
+ *
+ * Unlike creating a [KeylineList] using [keylineListOf], this method does not set unadjusted
+ * offsets by calculating them from a pivot index. This method simply interpolates all values of
+ * all keylines between the given pair.
+ */
+@VisibleForTesting
+internal fun lerp(
+    from: KeylineList,
+    to: KeylineList,
+    fraction: Float
+): KeylineList {
+    val interpolatedKeylines = from.fastMapIndexed { i, k ->
+        lerp(k, to[i], fraction)
+    }
+    return KeylineList(interpolatedKeylines)
+}
+
+private fun lerp(
+    outputMin: Float,
+    outputMax: Float,
+    inputMin: Float,
+    inputMax: Float,
+    value: Float
+): Float {
+    if (value <= inputMin) {
+        return outputMin
+    } else if (value >= inputMax) {
+        return outputMax
+    }
+
+    return lerp(outputMin, outputMax, (value - inputMin) / (inputMax - inputMin))
+}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 06055aa..6b5fb34 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -751,6 +751,11 @@
     enum_constant public static final androidx.compose.ui.graphics.PathIterator.ConicEvaluation AsQuadratics;
   }
 
+  public final class PathKt {
+    method public static androidx.compose.ui.graphics.Path Path();
+    method public static androidx.compose.ui.graphics.Path copy(androidx.compose.ui.graphics.Path);
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
     method public long getPosition(float distance);
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index d19c9a5..9ca7f8b 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -787,6 +787,11 @@
     enum_constant public static final androidx.compose.ui.graphics.PathIterator.ConicEvaluation AsQuadratics;
   }
 
+  public final class PathKt {
+    method public static androidx.compose.ui.graphics.Path Path();
+    method public static androidx.compose.ui.graphics.Path copy(androidx.compose.ui.graphics.Path);
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
     method public long getPosition(float distance);
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
similarity index 96%
rename from compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
rename to compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
index cd9b92d..cfe1192 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
+++ b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -18,6 +18,10 @@
 
 import androidx.compose.ui.graphics.colorspace.ColorSpaces
 import androidx.compose.ui.util.lerp
+import androidx.core.graphics.blue
+import androidx.core.graphics.green
+import androidx.core.graphics.red
+import kotlin.math.abs
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -510,7 +514,7 @@
             0xff5649cd, 0xff5649ce, 0xff5649ce, 0xff5549ce, 0xff5549ce, 0xff5549ce, 0xff5549cf,
             0xff5449cf, 0xff5448cf, 0xff5448cf, 0xff5448cf, 0xff5348cf, 0xff5348d0, 0xff5348d0,
             0xff5348d0, 0xff5248d0, 0xff5248d0, 0xff5248d1, 0xff5247d1, 0xff5147d1, 0xff5147d1,
-            0xff5147d1, 0xff5147d2, 0xff5047d2, 0xff5047d2, 0xff5047d2, 0xff5047d2, 0xff4f47d2,
+            0xff5147d1, 0xff5147d1, 0xff5047d2, 0xff5047d2, 0xff5047d2, 0xff5047d2, 0xff4f47d2,
             0xff4f46d3, 0xff4f46d3, 0xff4f46d3, 0xff4e46d3, 0xff4e46d3, 0xff4e46d4, 0xff4e46d4,
             0xff4d46d4, 0xff4d46d4, 0xff4d46d4, 0xff4d45d4, 0xff4c45d5, 0xff4c45d5, 0xff4c45d5,
             0xff4c45d5, 0xff4b45d5, 0xff4b45d5, 0xff4b45d6, 0xff4b45d6, 0xff4a44d6, 0xff4a44d6,
@@ -551,7 +555,17 @@
         repeat(1001) {
             val color = lerp(Color.Red, Color.Blue, it / 1000f)
             val colorLong = color.toArgb().toLong() and 0xFFFFFFFFL
-            assertEquals(expected[it], colorLong,
+            // Check individual color channels to account for possible float and half-float
+            // precision issues across devices. We allow up to 1/255 of difference
+            assertTrue(abs(expected[it].toInt().red - color.toArgb().red) < 2,
+                "Expected fraction $it/1000 to have color " +
+                    "0x${expected[it].toString(16)}, but was 0x${colorLong.toString(16)}"
+            )
+            assertTrue(abs(expected[it].toInt().green - color.toArgb().green) < 2,
+                "Expected fraction $it/1000 to have color " +
+                    "0x${expected[it].toString(16)}, but was 0x${colorLong.toString(16)}"
+            )
+            assertTrue(abs(expected[it].toInt().blue - color.toArgb().blue) < 2,
                 "Expected fraction $it/1000 to have color " +
                     "0x${expected[it].toString(16)}, but was 0x${colorLong.toString(16)}"
             )
diff --git a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
index 1952be6..0f019e5 100644
--- a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
+++ b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
@@ -128,6 +128,33 @@
         }
     }
 
+    @Test
+    fun testPathCopy() {
+        val r1 = Path().apply {
+            addRect(
+                Rect(0.0f, 0.0f, 10.0f, 10.0f),
+                Path.Direction.ClockWise
+            )
+        }
+        val r2 = r1.copy().apply {
+            addRect(
+                Rect(5.0f, 5.0f, 15.0f, 15.0f),
+                Path.Direction.ClockWise
+            )
+        }
+
+        val r1Bounds = r1.getBounds()
+
+        // Additional changes to a copied path should not mutate the original path
+        val expectedR1Bounds = Rect(0.0f, 0.0f, 10.0f, 10.0f)
+        assertEquals(expectedR1Bounds, r1Bounds)
+
+        val r2bounds = r2.getBounds()
+
+        val expectedR2Bounds = Rect(0.0f, 0.0f, 15.0f, 15.0f)
+        assertEquals(expectedR2Bounds, r2bounds)
+    }
+
     class TestAndroidPath : android.graphics.Path() {
 
         var resetCount = 0
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt
new file mode 100644
index 0000000..5858e3d
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidFloat16.android.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 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.ui.graphics
+
+import android.os.Build
+import android.util.Half
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+// Use the platform version to benefit from ART intrinsics on API 30+
+@Suppress("NOTHING_TO_INLINE")
+internal actual inline fun floatToHalf(f: Float): Short = if (Build.VERSION.SDK_INT >= 26) {
+    Api26Impl.floatToHalf(f)
+} else {
+    softwareFloatToHalf(f)
+}
+
+// Use the platform version to benefit from ART intrinsics on API 30+
+@Suppress("NOTHING_TO_INLINE")
+internal actual inline fun halfToFloat(h: Short): Float = if (Build.VERSION.SDK_INT >= 26) {
+    Api26Impl.halfToFloat(h)
+} else {
+    softwareHalfToFloat(h)
+}
+
+@RequiresApi(26)
+internal object Api26Impl {
+    @JvmStatic
+    @DoNotInline
+    @Suppress("HalfFloat")
+    fun floatToHalf(f: Float) = Half.toHalf(f)
+
+    @JvmStatic
+    @DoNotInline
+    @Suppress("HalfFloat")
+    fun halfToFloat(h: Short) = Half.toFloat(h)
+}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
index 8aabeca6..27b2ece 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
@@ -147,37 +147,7 @@
      * @return The half-precision float value represented by this object
      * converted to type `Float`
      */
-    fun toFloat(): Float {
-        val bits = halfValue.toInt() and 0xffff
-        val s = bits and Fp16SignMask
-        val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
-        val m = bits and Fp16SignificandMask
-
-        var outE = 0
-        var outM = 0
-
-        if (e == 0) { // Denormal or 0
-            if (m != 0) {
-                // Convert denorm fp16 into normalized fp32
-                var o = floatFromBits(Fp32DenormalMagic + m)
-                o -= Fp32DenormalFloat
-                return if (s == 0) o else -o
-            }
-        } else {
-            outM = m shl 13
-            if (e == 0x1f) { // Infinite or NaN
-                outE = 0xff
-                if (outM != 0) { // SNaNs are quieted
-                    outM = outM or Fp32QNaNMask
-                }
-            } else {
-                outE = e - Fp16ExponentBias + Fp32ExponentBias
-            }
-        }
-
-        val out = s shl 16 or (outE shl Fp32ExponentShift) or outM
-        return floatFromBits(out)
-    }
+    fun toFloat(): Float = halfToFloat(halfValue)
 
     /**
      * Returns the value of this `Float16` as a `Double` after
@@ -629,7 +599,11 @@
  * Convert a single-precision float to a half-precision float, stored as
  * [Short] data type to hold its 16 bits.
  */
-internal fun floatToHalf(f: Float): Short {
+internal expect fun floatToHalf(f: Float): Short
+
+// Provided here as a convenience for `actual` implementations
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun softwareFloatToHalf(f: Float): Short {
     val bits = f.toRawBits()
     val s = bits.ushr(Fp32SignShift)
     var e = bits.ushr(Fp32ExponentShift) and Fp32ExponentMask
@@ -673,7 +647,11 @@
 /**
  * Convert a half-precision float to a single-precision float.
  */
-internal fun halfToFloat(h: Short): Float {
+internal expect fun halfToFloat(h: Short): Float
+
+// Provided here as a convenience for `actual` implementations
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun softwareHalfToFloat(h: Short): Float {
     val bits = h.toInt() and 0xffff
     val s = bits and Fp16SignMask
     val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index 1b339d6..fe975e2 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -23,6 +23,11 @@
 
 expect fun Path(): Path
 
+/**
+ * Create a new path, copying the contents from the src path.
+ */
+fun Path.copy(): Path = Path().apply { addPath(this@copy) }
+
 @JvmDefaultWithCompatibility
 /* expect class */ interface Path {
     /**
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt
new file mode 100644
index 0000000..c102496
--- /dev/null
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopFloat16.desktop.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 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.ui.graphics
+
+internal actual fun floatToHalf(f: Float): Short = softwareFloatToHalf(f)
+
+internal actual fun halfToFloat(h: Short): Float = softwareHalfToFloat(h)
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index 8f15c3f..0364dec 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -6,6 +6,7 @@
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void cancelAndRecreateRecomposer();
     method public A getActivity();
     method public R getActivityRule();
     method public androidx.compose.ui.unit.Density getDensity();
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index 8f15c3f..0364dec 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -6,6 +6,7 @@
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public void cancelAndRecreateRecomposer();
     method public A getActivity();
     method public R getActivityRule();
     method public androidx.compose.ui.unit.Density getDensity();
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 626ca1e3..0ee15e0 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -338,6 +338,10 @@
     ): SemanticsNodeInteractionCollection = composeTest.onAllNodes(matcher, useUnmergedTree)
 
     override fun setContent(composable: @Composable () -> Unit) = composeTest.setContent(composable)
+
+    fun cancelAndRecreateRecomposer() {
+        environment.cancelAndRecreateRecomposer()
+    }
 }
 
 private fun <A : ComponentActivity> getActivityFromTestRule(rule: ActivityScenarioRule<A>): A {
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index f71c259..c292274 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -29,6 +29,7 @@
 
   @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public abstract class AndroidComposeUiTestEnvironment<A extends androidx.activity.ComponentActivity> {
     ctor public AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext);
+    method public final void cancelAndRecreateRecomposer();
     method protected abstract A? getActivity();
     method public final androidx.compose.ui.test.AndroidComposeUiTest<A> getTest();
     method public final <R> R runTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends R> block);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 44fd399..f60a25f 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -29,6 +29,7 @@
 
   @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public abstract class AndroidComposeUiTestEnvironment<A extends androidx.activity.ComponentActivity> {
     ctor public AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext);
+    method public final void cancelAndRecreateRecomposer();
     method protected abstract A? getActivity();
     method public final androidx.compose.ui.test.AndroidComposeUiTest<A> getTest();
     method public final <R> R runTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends R> block);
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index e8fd13b..72a79c6 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -219,26 +219,30 @@
 @ExperimentalTestApi
 @OptIn(InternalTestApi::class, ExperimentalCoroutinesApi::class, ExperimentalComposeUiApi::class)
 abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity>(
-    effectContext: CoroutineContext = EmptyCoroutineContext
+    private val effectContext: CoroutineContext = EmptyCoroutineContext
 ) {
     private val idlingResourceRegistry = IdlingResourceRegistry()
 
     internal val composeRootRegistry = ComposeRootRegistry()
 
     private val mainClockImpl: MainTestClockImpl
-    private val composeIdlingResource: ComposeIdlingResource
+    private lateinit var composeIdlingResource: ComposeIdlingResource
     private var idlingStrategy: IdlingStrategy = EspressoLink(idlingResourceRegistry)
 
-    private val recomposer: Recomposer
+    private lateinit var recomposer: Recomposer
     // We can only accept a TestDispatcher here because we need to access its scheduler.
     private val testCoroutineDispatcher = effectContext[ContinuationInterceptor] as? TestDispatcher
         ?: UnconfinedTestDispatcher()
     private val testCoroutineScope = TestScope(testCoroutineDispatcher)
-    private val recomposerCoroutineScope: CoroutineScope
+    private lateinit var recomposerCoroutineScope: CoroutineScope
     private val coroutineExceptionHandler = UncaughtExceptionHandler()
 
+    private val frameClock: TestMonotonicFrameClock
+    private val recomposerContinuationInterceptor: ApplyingContinuationInterceptor
+    private val infiniteAnimationPolicy: InfiniteAnimationPolicy
+
     init {
-        val frameClock = TestMonotonicFrameClock(
+        frameClock = TestMonotonicFrameClock(
             testCoroutineScope,
             // This callback will get run at the same time, relative to frame callbacks and
             // coroutine resumptions, as the Choreographer's perform traversal frame, where it runs
@@ -252,10 +256,12 @@
         )
         // The applying interceptor needs to be the outermost wrapper since TestMonotonicFrameClock
         // will not delegate if the dispatcher dispatch is not needed at the time of intercept.
-        val recomposerContinuationInterceptor =
+        recomposerContinuationInterceptor =
             ApplyingContinuationInterceptor(frameClock.continuationInterceptor)
+
         mainClockImpl = MainTestClockImpl(testCoroutineDispatcher.scheduler, frameClock)
-        val infiniteAnimationPolicy = object : InfiniteAnimationPolicy {
+
+        infiniteAnimationPolicy = object : InfiniteAnimationPolicy {
             override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R {
                 if (mainClockImpl.autoAdvance) {
                     throw CancellationException("Infinite animations are disabled on tests")
@@ -263,6 +269,11 @@
                 return block()
             }
         }
+
+        createRecomposer()
+    }
+
+    private fun createRecomposer() {
         recomposerCoroutineScope = CoroutineScope(
             effectContext +
                 recomposerContinuationInterceptor +
@@ -272,11 +283,17 @@
                 Job()
         )
         recomposer = Recomposer(recomposerCoroutineScope.coroutineContext)
+
         composeIdlingResource = ComposeIdlingResource(
             composeRootRegistry, mainClockImpl, recomposer
         )
     }
 
+    fun cancelAndRecreateRecomposer() {
+        recomposer.cancel()
+        createRecomposer()
+    }
+
     internal val testReceiverScope = AndroidComposeUiTestImpl()
     private val testOwner = AndroidTestOwner()
     private val testContext = createTestContext(testOwner)
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index fc128af9..4291d29 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -10,6 +10,7 @@
     ctor public AnnotatedString(String text, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.ParagraphStyle>> paragraphStyles);
     method public operator char get(int index);
     method public int getLength();
+    method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.LinkAnnotation>> getLinkAnnotations(int start, int end);
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.ParagraphStyle>> getParagraphStyles();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> getSpanStyles();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<java.lang.String>> getStringAnnotations(int start, int end);
@@ -17,6 +18,7 @@
     method public String getText();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.TtsAnnotation>> getTtsAnnotations(int start, int end);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.UrlAnnotation>> getUrlAnnotations(int start, int end);
+    method public boolean hasLinkAnnotations(int start, int end);
     method public boolean hasStringAnnotations(String tag, int start, int end);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.AnnotatedString plus(androidx.compose.ui.text.AnnotatedString other);
     method public androidx.compose.ui.text.AnnotatedString subSequence(int startIndex, int endIndex);
@@ -32,6 +34,8 @@
     ctor public AnnotatedString.Builder(androidx.compose.ui.text.AnnotatedString text);
     ctor public AnnotatedString.Builder(optional int capacity);
     ctor public AnnotatedString.Builder(String text);
+    method public void addLink(androidx.compose.ui.text.LinkAnnotation.Clickable clickable, int start, int end);
+    method public void addLink(androidx.compose.ui.text.LinkAnnotation.Url url, int start, int end);
     method public void addStringAnnotation(String tag, String annotation, int start, int end);
     method public void addStyle(androidx.compose.ui.text.ParagraphStyle style, int start, int end);
     method public void addStyle(androidx.compose.ui.text.SpanStyle style, int start, int end);
@@ -47,6 +51,7 @@
     method public int getLength();
     method public void pop();
     method public void pop(int index);
+    method public int pushLink(androidx.compose.ui.text.LinkAnnotation link);
     method public int pushStringAnnotation(String tag, String annotation);
     method public int pushStyle(androidx.compose.ui.text.ParagraphStyle style);
     method public int pushStyle(androidx.compose.ui.text.SpanStyle style);
@@ -87,6 +92,7 @@
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString toLowerCase(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString toUpperCase(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
+    method public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.LinkAnnotation link, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.TtsAnnotation ttsAnnotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.UrlAnnotation urlAnnotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, String tag, String annotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
@@ -111,6 +117,21 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
+  public abstract class LinkAnnotation {
+  }
+
+  public static final class LinkAnnotation.Clickable extends androidx.compose.ui.text.LinkAnnotation {
+    ctor public LinkAnnotation.Clickable(String tag);
+    method public String getTag();
+    property public final String tag;
+  }
+
+  public static final class LinkAnnotation.Url extends androidx.compose.ui.text.LinkAnnotation {
+    ctor public LinkAnnotation.Url(String url);
+    method public String getUrl();
+    property public final String url;
+  }
+
   public final class MultiParagraph {
     ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
     ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index cd07d3f..fb565d4 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -10,6 +10,7 @@
     ctor public AnnotatedString(String text, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.ParagraphStyle>> paragraphStyles);
     method public operator char get(int index);
     method public int getLength();
+    method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.LinkAnnotation>> getLinkAnnotations(int start, int end);
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.ParagraphStyle>> getParagraphStyles();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> getSpanStyles();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<java.lang.String>> getStringAnnotations(int start, int end);
@@ -17,6 +18,7 @@
     method public String getText();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.TtsAnnotation>> getTtsAnnotations(int start, int end);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.UrlAnnotation>> getUrlAnnotations(int start, int end);
+    method public boolean hasLinkAnnotations(int start, int end);
     method public boolean hasStringAnnotations(String tag, int start, int end);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.AnnotatedString plus(androidx.compose.ui.text.AnnotatedString other);
     method public androidx.compose.ui.text.AnnotatedString subSequence(int startIndex, int endIndex);
@@ -32,6 +34,8 @@
     ctor public AnnotatedString.Builder(androidx.compose.ui.text.AnnotatedString text);
     ctor public AnnotatedString.Builder(optional int capacity);
     ctor public AnnotatedString.Builder(String text);
+    method public void addLink(androidx.compose.ui.text.LinkAnnotation.Clickable clickable, int start, int end);
+    method public void addLink(androidx.compose.ui.text.LinkAnnotation.Url url, int start, int end);
     method public void addStringAnnotation(String tag, String annotation, int start, int end);
     method public void addStyle(androidx.compose.ui.text.ParagraphStyle style, int start, int end);
     method public void addStyle(androidx.compose.ui.text.SpanStyle style, int start, int end);
@@ -47,6 +51,7 @@
     method public int getLength();
     method public void pop();
     method public void pop(int index);
+    method public int pushLink(androidx.compose.ui.text.LinkAnnotation link);
     method public int pushStringAnnotation(String tag, String annotation);
     method public int pushStyle(androidx.compose.ui.text.ParagraphStyle style);
     method public int pushStyle(androidx.compose.ui.text.SpanStyle style);
@@ -87,6 +92,7 @@
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString toLowerCase(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString toUpperCase(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
+    method public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.LinkAnnotation link, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.TtsAnnotation ttsAnnotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.UrlAnnotation urlAnnotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
     method @SuppressCompatibility @androidx.compose.ui.text.ExperimentalTextApi public static inline <R> R withAnnotation(androidx.compose.ui.text.AnnotatedString.Builder, String tag, String annotation, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
@@ -111,6 +117,21 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
+  public abstract class LinkAnnotation {
+  }
+
+  public static final class LinkAnnotation.Clickable extends androidx.compose.ui.text.LinkAnnotation {
+    ctor public LinkAnnotation.Clickable(String tag);
+    method public String getTag();
+    property public final String tag;
+  }
+
+  public static final class LinkAnnotation.Url extends androidx.compose.ui.text.LinkAnnotation {
+    ctor public LinkAnnotation.Url(String url);
+    method public String getUrl();
+    property public final String url;
+  }
+
   public final class MultiParagraph {
     ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
     ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
diff --git a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
index 7e8e019..ec4ea6c 100644
--- a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
+++ b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
@@ -1125,6 +1125,44 @@
         assertThat(buildResult.getStringAnnotations(tag, 10, 11)).isEmpty()
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun getAnnotation_separates_linkAnnotation_and_stringAnnotation() {
+        val annotation1 = LinkAnnotation.Url("url")
+        val annotation2 = LinkAnnotation.Clickable("clickable tag")
+        val annotation3 = "annotation"
+        val tag = "tag"
+        val buildResult = AnnotatedString.Builder().apply {
+            pushLink(annotation1)
+            append("ab")
+            pushLink(annotation2)
+            append("cd")
+            pushStringAnnotation(tag, annotation3)
+            append("ef")
+            pop()
+        }.toAnnotatedString()
+
+        // The final result is abcdef
+        //                     [    ] LinkAnnotation.Url
+        //                       [  ] LinkAnnotation.Clickable
+        //                         [] Range<String>
+        assertThat(buildResult.getLinkAnnotations(0, 2)).isEqualTo(
+            listOf(Range(annotation1, 0, 6, ""))
+        )
+        assertThat(buildResult.getLinkAnnotations(3, 5)).isEqualTo(
+            listOf(
+                Range(annotation1, 0, 6, ""),
+                Range(annotation2, 2, 6, "")
+            )
+        )
+
+        assertThat(buildResult.getStringAnnotations(0, 4)).isEmpty()
+        assertThat(buildResult.getStringAnnotations(4, 6)).isEqualTo(
+            listOf(Range(annotation3, 4, 6, tag))
+        )
+        assertThat(buildResult.getStringAnnotations("another tag", 4, 6)).isEmpty()
+    }
+
     private fun createAnnotatedString(
         text: String,
         color: Color = Color.Red,
diff --git a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
index 8365149..a51904d 100644
--- a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
+++ b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/SaversTest.kt
@@ -348,6 +348,8 @@
             withAnnotation(VerbatimTtsAnnotation("verbatim2")) { append("4") }
             withAnnotation(UrlAnnotation("url1")) { append("5") }
             withAnnotation(UrlAnnotation("url2")) { append("6") }
+            withAnnotation(LinkAnnotation.Url("url3")) { append("7") }
+            withAnnotation(LinkAnnotation.Clickable("tag3")) { append("8") }
         }
 
         val saved = with(AnnotatedStringSaver) { defaultSaverScope.save(original) }
@@ -371,6 +373,8 @@
             withAnnotation(VerbatimTtsAnnotation("verbatim2")) { append("8") }
             withAnnotation(UrlAnnotation("url1")) { append("9") }
             withAnnotation(UrlAnnotation("url2")) { append("10") }
+            withAnnotation(LinkAnnotation.Url("url3")) { append("11") }
+            withAnnotation(LinkAnnotation.Clickable("tag3")) { append("12") }
         }
 
         val saved = with(AnnotatedStringSaver) { defaultSaverScope.save(original) }
diff --git a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt
index 48da0c4..d4934fd 100644
--- a/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt
+++ b/compose/ui/ui-text/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextFieldValueTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextRange
@@ -177,6 +178,8 @@
             withAnnotation(VerbatimTtsAnnotation("verbatim2")) { append("6") }
             withAnnotation(UrlAnnotation("url1")) { append("7") }
             withAnnotation(UrlAnnotation("url2")) { append("8") }
+            withAnnotation(LinkAnnotation.Url("url3")) { append("9") }
+            withAnnotation(LinkAnnotation.Clickable("tag3")) { append("10") }
             withStyle(
                 SpanStyle(
                     color = Color.Red,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 6d05c99..10058d2 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -216,6 +216,29 @@
             it.item is UrlAnnotation && intersect(start, end, it.start, it.end)
         } ?: emptyList()) as List<Range<UrlAnnotation>>)
 
+    /**
+     * Query all of the [LinkAnnotation]s attached on this [AnnotatedString].
+     *
+     * @param start the start of the query range, inclusive.
+     * @param end the end of the query range, exclusive.
+     * @return a list of annotations stored in [Range].  Notice that All annotations that intersect
+     * with the range [start, end) will be returned. When [start] is bigger than [end], an empty
+     * list will be returned.
+     */
+    @Suppress("UNCHECKED_CAST")
+    fun getLinkAnnotations(start: Int, end: Int): List<Range<LinkAnnotation>> =
+        ((annotations?.fastFilter {
+            it.item is LinkAnnotation && intersect(start, end, it.start, it.end)
+        } ?: emptyList()) as List<Range<LinkAnnotation>>)
+
+    /**
+     * Returns true if [getLinkAnnotations] with the same parameters would return a non-empty list
+     */
+    fun hasLinkAnnotations(start: Int, end: Int): Boolean =
+        annotations?.fastAny {
+            it.item is LinkAnnotation && intersect(start, end, it.start, it.end)
+        } ?: false
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is AnnotatedString) return false
@@ -512,6 +535,41 @@
         }
 
         /**
+         * Set a [LinkAnnotation.Url] for the given [range].
+         *
+         * When clicking on the text in [range], the corresponding URL from the [url] annotation
+         * will be opened using [androidx.compose.ui.platform.UriHandler].
+         *
+         * URLs may be treated specially by screen readers, including being identified while reading text with an audio icon or being
+         * summarized in a links menu. When the text
+         *
+         * @param url A [LinkAnnotation.Url] object that stores the URL being linked to.
+         * @param start the inclusive starting offset of the range
+         * @param end the exclusive end offset of the range
+         * @see getStringAnnotations
+         */
+        @Suppress("SetterReturnsThis")
+        fun addLink(url: LinkAnnotation.Url, start: Int, end: Int) {
+            annotations.add(MutableRange(url, start, end))
+        }
+
+        /**
+         * Set a [LinkAnnotation.Clickable] for the given [range].
+         *
+         * When clicking on the text in [range], the handler will be triggered with the tag
+         * corresponding to the [clickable] object.
+         *
+         * @param clickable A [LinkAnnotation.Clickable] object that stores the tag being linked to.
+         * @param start the inclusive starting offset of the range
+         * @param end the exclusive end offset of the range
+         * @see getStringAnnotations
+         */
+        @Suppress("SetterReturnsThis")
+        fun addLink(clickable: LinkAnnotation.Clickable, start: Int, end: Int) {
+            annotations.add(MutableRange(clickable, start, end))
+        }
+
+        /**
          * Applies the given [SpanStyle] to any appended text until a corresponding [pop] is
          * called.
          *
@@ -602,6 +660,24 @@
         }
 
         /**
+         * Attach the given [LinkAnnotation] to any appended text until a corresponding [pop]
+         * is called.
+         *
+         * @param link A [LinkAnnotation] object that stores the URL or clickable tag being
+         * linked to.
+         * @see getStringAnnotations
+         * @see Range
+         */
+        @Suppress("BuilderSetStyle")
+        fun pushLink(link: LinkAnnotation): Int {
+            MutableRange(item = link, start = text.length).also {
+                styleStack.add(it)
+                annotations.add(it)
+            }
+            return styleStack.size - 1
+        }
+
+        /**
          * Ends the style or annotation that was added via a push operation before.
          *
          * @see pushStyle
@@ -1045,6 +1121,30 @@
 }
 
 /**
+ * Pushes an [LinkAnnotation] to the [AnnotatedString.Builder], executes [block] and then pops the
+ * annotation.
+ *
+ * @param link A [LinkAnnotation] object that stores the URL or clic being linked to.
+ * @param block function to be executed
+ *
+ * @return result of the [block]
+ *
+ * @see AnnotatedString.Builder.pushStringAnnotation
+ * @see AnnotatedString.Builder.pop
+ */
+inline fun <R : Any> Builder.withAnnotation(
+    link: LinkAnnotation,
+    crossinline block: Builder.() -> R
+): R {
+    val index = pushLink(link)
+    return try {
+        block(this)
+    } finally {
+        pop(index)
+    }
+}
+
+/**
  * Filter the range list based on [Range.start] and [Range.end] to include ranges only in the range
  * of [start] (inclusive) and [end] (exclusive).
  *
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
new file mode 100644
index 0000000..c46ab75
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 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.ui.text
+
+/**
+ * An annotation that represents a clickable part of the text.
+ *
+ * Disclaimer: This is a no-op at the moment. Continue using [UrlAnnotation] to make your links
+ * visible for the accessibility services like Talkback
+ */
+abstract class LinkAnnotation private constructor() {
+    /**
+     * An annotation that contains a url string. When clicking on the text to which this annotation
+     * is attached, the app will try to open the url using [androidx.compose.ui.platform.UriHandler].
+     */
+    class Url(val url: String) : LinkAnnotation() {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Url) return false
+            return url == other.url
+        }
+
+        override fun hashCode(): Int {
+            return url.hashCode()
+        }
+
+        override fun toString(): String {
+            return "LinkAnnotation.Url(url=$url)"
+        }
+    }
+
+    /**
+     * An annotation that contains a clickable marked with [tag]. When clicking on the text to
+     * which this annotation is attached, the app will trigger a handler's callback.
+     *
+     * Disclaimer: This is a no-op at the moment. Continue using
+     * [androidx.compose.foundation.text.ClickableText] to make your text clickable.
+     */
+    class Clickable(val tag: String) : LinkAnnotation() {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Clickable) return false
+            return tag == other.tag
+        }
+
+        override fun hashCode(): Int {
+            return tag.hashCode()
+        }
+
+        override fun toString(): String {
+            return "LinkAnnotation.Clickable(tag=$tag)"
+        }
+    }
+}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
index 52ae369..7a461927 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
@@ -115,7 +115,9 @@
     Paragraph,
     Span,
     VerbatimTts,
-    Url,
+    Url, // UrlAnnotation
+    Link, // LinkAnnotation.Url
+    Clickable,
     String
 }
 
@@ -127,6 +129,8 @@
             is SpanStyle -> AnnotationType.Span
             is VerbatimTtsAnnotation -> AnnotationType.VerbatimTts
             is UrlAnnotation -> AnnotationType.Url
+            is LinkAnnotation.Url -> AnnotationType.Link
+            is LinkAnnotation.Clickable -> AnnotationType.Clickable
             else -> AnnotationType.String
         }
 
@@ -143,6 +147,16 @@
                 UrlAnnotationSaver,
                 this
             )
+            AnnotationType.Link -> save(
+                it.item as LinkAnnotation.Url,
+                LinkSaver,
+                this
+            )
+            AnnotationType.Clickable -> save(
+                it.item as LinkAnnotation.Clickable,
+                ClickableSaver,
+                this
+            )
             AnnotationType.String -> save(it.item)
         }
 
@@ -179,6 +193,14 @@
                 val item: UrlAnnotation = restore(list[1], UrlAnnotationSaver)!!
                 AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
             }
+            AnnotationType.Link -> {
+                val item: LinkAnnotation.Url = restore(list[1], LinkSaver)!!
+                AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
+            }
+            AnnotationType.Clickable -> {
+                val item: LinkAnnotation.Clickable = restore(list[1], ClickableSaver)!!
+                AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
+            }
             AnnotationType.String -> {
                 val item: String = restore(list[1])!!
                 AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
@@ -198,6 +220,16 @@
     restore = { UrlAnnotation(restore(it)!!) }
 )
 
+private val LinkSaver = Saver<LinkAnnotation.Url, Any>(
+    save = { save(it.url) },
+    restore = { LinkAnnotation.Url(restore(it)!!) }
+)
+
+private val ClickableSaver = Saver<LinkAnnotation.Clickable, Any>(
+    save = { save(it.tag) },
+    restore = { LinkAnnotation.Clickable(restore(it)!!) }
+)
+
 internal val ParagraphStyleSaver = Saver<ParagraphStyle, Any>(
     save = {
         arrayListOf(
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/TogglePlacementInLookaheadScope.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/TogglePlacementInLookaheadScope.kt
new file mode 100644
index 0000000..2b79e6d
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/TogglePlacementInLookaheadScope.kt
@@ -0,0 +1,308 @@
+/*
+ * 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.ui.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReusableContent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertTrue
+import org.junit.Assert.assertFalse
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TogglePlacementInLookaheadScope {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @get:Rule
+    val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
+
+    @Test
+    fun togglePlacement() {
+        var place by mutableStateOf(true)
+        var newChildAdded by mutableStateOf(false)
+        val placed = mutableListOf(false, false)
+        rule.setContent {
+            LookaheadScope {
+                Layout(content = {
+                    TestItem(
+                        newChildAdded,
+                        placed = placed
+                    )
+                }) { list, constraints ->
+                    val placeables = list.map { it.measure(constraints) }
+                    layout(placeables[0].width, placeables[0].height) {
+                        if (place) {
+                            placeables.forEach { it.place(0, 0) }
+                        }
+                    }
+                }
+            }
+        }
+        rule.runOnIdle {
+            // Avoid placing the children and at the same time add a new child to the subtree
+            newChildAdded = true
+            place = false
+        }
+        rule.waitForIdle()
+        assertFalse(placed[0])
+        assertFalse(placed[1])
+        rule.runOnIdle {
+            place = true
+        }
+        rule.waitForIdle()
+        assertTrue(placed[0])
+        assertTrue(placed[1])
+    }
+
+    @Test
+    fun togglePlacementInModifier() {
+        var place by mutableStateOf(true)
+        var newChildAdded by mutableStateOf(false)
+        rule.setContent {
+            LookaheadScope {
+                TestItem(
+                    newChildAdded,
+                    Modifier.layout { m, constraints ->
+                        val p = m.measure(constraints)
+                        layout(p.width, p.height) {
+                            if (place) {
+                                p.place(0, 0)
+                            }
+                        }
+                    },
+                )
+            }
+        }
+        rule.runOnIdle {
+            // Avoid placing the children and at the same time add a new child to the subtree
+            newChildAdded = true
+            place = false
+        }
+        rule.waitForIdle()
+        rule.runOnIdle {
+            place = true
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun reusableContentInLookahead() {
+        var place by mutableStateOf(true)
+        var reusableContentKey by mutableStateOf(1)
+        val placed = mutableListOf(false, false)
+        rule.setContent {
+            LookaheadScope {
+                Layout(measurePolicy =
+                { list, constraints ->
+                    val placeables = list.map { it.measure(constraints) }
+                    layout(placeables[0].width, placeables[0].height) {
+                        if (place) {
+                            placeables.forEach { it.place(0, 0) }
+                        }
+                    }
+                }, content = {
+                    Card {
+                        Column {
+                            Image(
+                                painter = ColorPainter(Color.Blue),
+                                contentDescription = null,
+                                contentScale = ContentScale.Crop,
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .aspectRatio(16f / 9f)
+                            )
+
+                            ReusableContent(reusableContentKey) {
+                                Row(
+                                    horizontalArrangement = Arrangement.spacedBy(4.dp),
+                                    verticalAlignment = Alignment.CenterVertically,
+                                    modifier = Modifier
+                                        .padding(
+                                            start = 12.dp,
+                                            top = 8.dp,
+                                            end = 12.dp,
+                                            bottom = 12.dp
+                                        )
+                                        .fillMaxWidth()
+                                        .layout { measurable, constraints ->
+                                            measurable
+                                                .measure(constraints)
+                                                .run {
+                                                    layout(width, height) {
+                                                        if (isLookingAhead) {
+                                                            placed[0] = true
+                                                        } else {
+                                                            placed[1] = true
+                                                        }
+                                                        @Suppress("UNUSED_EXPRESSION")
+                                                        reusableContentKey // force a read
+                                                        place(0, 0)
+                                                    }
+                                                }
+                                        }
+                                ) {
+
+                                    Text(
+                                        text = "Static text",
+                                        color = Color.White,
+                                        modifier = Modifier
+                                            .background(Color.Gray, RoundedCornerShape(2.dp))
+                                            .padding(
+                                                start = 3.dp, end = 3.dp,
+                                                top = 0.5.dp, bottom = 1.dp
+                                            )
+                                    )
+
+                                    val badgeModifier = Modifier
+                                        .border(0.5f.dp, Color.Black, RoundedCornerShape(2.dp))
+                                        .padding(
+                                            start = 3.dp, end = 3.dp,
+                                            top = 0.5.dp, bottom = 1.dp
+                                        )
+
+                                    Text(
+                                        text = "Updated",
+                                        modifier = badgeModifier,
+                                    )
+                                }
+                            }
+                        }
+                    }
+                })
+            }
+        }
+        assertTrue(placed[0])
+        assertTrue(placed[1])
+        placed[0] = false
+        placed[1] = false
+
+        rule.runOnIdle {
+            place = false
+        }
+        rule.runOnIdle {
+            reusableContentKey++
+        }
+        assertFalse(placed[0])
+        assertFalse(placed[1])
+
+        rule.runOnIdle {
+            place = true
+        }
+        rule.waitForIdle()
+        assertTrue(placed[0])
+        assertTrue(placed[1])
+        rule.waitForIdle()
+    }
+
+    @Composable
+    private fun TestItem(
+        showNewText: Boolean,
+        modifier: Modifier = Modifier,
+        placed: MutableList<Boolean> = mutableListOf(false, false)
+    ) {
+        Card(modifier) {
+            Column {
+                Image(
+                    painter = ColorPainter(Color.Blue),
+                    contentDescription = null,
+                    contentScale = ContentScale.Crop,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .aspectRatio(16f / 9f)
+                )
+
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(4.dp),
+                    verticalAlignment = Alignment.CenterVertically,
+                    modifier = Modifier
+                        .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 12.dp)
+                        .fillMaxWidth()
+                ) {
+
+                    Text(
+                        text = "Static text",
+                        color = Color.White,
+                        modifier = Modifier
+                            .background(Color.Gray, RoundedCornerShape(2.dp))
+                            .padding(
+                                start = 3.dp, end = 3.dp,
+                                top = 0.5.dp, bottom = 1.dp
+                            )
+                    )
+
+                    val badgeModifier = Modifier
+                        .border(0.5f.dp, Color.Black, RoundedCornerShape(2.dp))
+                        .padding(
+                            start = 3.dp, end = 3.dp,
+                            top = 0.5.dp, bottom = 1.dp
+                        )
+
+                    if (showNewText) {
+                        Text(
+                            text = "New",
+                            modifier = badgeModifier.layout { measurable, constraints ->
+                                measurable.measure(constraints).run {
+                                    layout(width, height) {
+                                        if (isLookingAhead) {
+                                            placed[0] = true
+                                        } else {
+                                            placed[1] = true
+                                        }
+                                        place(0, 0)
+                                    }
+                                }
+                            },
+                        )
+                    }
+
+                    Text(
+                        text = "Updated",
+                        modifier = badgeModifier,
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
index 0b898b5..90f015a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt
@@ -151,7 +151,7 @@
 
     /**
      * Enables the display of visual layout bounds for the Compose UI content of this view.
-     * This is typically managed
+     * This is typically configured using the system developer setting for "Show layout bounds."
      */
     @OptIn(InternalCoreApi::class)
     @InternalComposeUiApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 80114fd..36281ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -978,16 +978,17 @@
                 // this node was scheduled for remeasure or relayout while it was not
                 // placed. such requests are ignored for non-placed nodes so we have to
                 // re-schedule remeasure or relayout.
-                if (it.measurePending) {
-                    it.requestRemeasure(forceRequest = true)
-                } else if (it.layoutPending) {
-                    it.requestRelayout(forceRequest = true)
-                } else if (it.lookaheadMeasurePending) {
+                if (it.lookaheadMeasurePending) {
                     it.requestLookaheadRemeasure(forceRequest = true)
-                } else if (it.lookaheadLayoutPending) {
-                    it.requestLookaheadRelayout(forceRequest = true)
                 } else {
-                    // no extra work required and node is ready to be displayed
+                    if (it.lookaheadLayoutPending) {
+                        it.requestLookaheadRelayout(forceRequest = true)
+                    }
+                    if (it.measurePending) {
+                        it.requestRemeasure(forceRequest = true)
+                    } else if (it.layoutPending) {
+                        it.requestRelayout(forceRequest = true)
+                    }
                 }
             }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 81e0316..fe78904 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -718,6 +718,10 @@
                 }
             }
 
+            checkPrecondition(lookaheadPassDelegate?.placedOnce != false) {
+                "Error: Placement happened before lookahead."
+            }
+
             // Post-lookahead (if any) placement
             placeOuterCoordinator(position, zIndex, layerBlock)
         }
@@ -1035,7 +1039,7 @@
         internal val measurePassDelegate: MeasurePassDelegate
             get() = this@LayoutNodeLayoutDelegate.measurePassDelegate
         internal var duringAlignmentLinesQuery: Boolean = false
-        private var placedOnce: Boolean = false
+        internal var placedOnce: Boolean = false
         private var measuredOnce: Boolean = false
         val lastConstraints: Constraints?
             get() = lookaheadConstraints
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
index 157369b..9cd3a5d 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
@@ -106,7 +106,7 @@
             val nearbyLocalAddress = uwbClient.localAddress.await()
             val nearbyRangingCapabilities = uwbClient.rangingCapabilities.await()
             val localAddress = UwbAddress(nearbyLocalAddress.address)
-            val supportedConfigIds = nearbyRangingCapabilities.supportedConfigIds
+            val supportedConfigIds = nearbyRangingCapabilities.supportedConfigIds.toMutableList()
             supportedConfigIds.retainAll(PUBLIC_AVAILABLE_CONFIG_IDS)
             val rangingCapabilities = RangingCapabilities(
                 nearbyRangingCapabilities.supportsDistance(),
@@ -161,7 +161,8 @@
                     it.minRangingInterval,
                     it.supportedChannels.toSet(),
                     it.supportedNtfConfigs.toSet(),
-                    it.supportedConfigIds.filter { it in PUBLIC_AVAILABLE_CONFIG_IDS }.toSet(),
+                    it.supportedConfigIds.toMutableList()
+                        .filter { it in PUBLIC_AVAILABLE_CONFIG_IDS }.toSet(),
                     it.supportedSlotDurations.toSet(),
                     it.supportedRangingUpdateRates.toSet(),
                     it.supportsRangingIntervalReconfigure,
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 6cdb2b3..5be2e23 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -24,7 +24,7 @@
     docsWithoutApiSince("androidx.ads:ads-identifier-common:1.0.0-alpha05")
     docsWithoutApiSince("androidx.ads:ads-identifier-provider:1.0.0-alpha05")
     kmpDocs("androidx.annotation:annotation:1.7.1")
-    docs("androidx.annotation:annotation-experimental:1.4.0-beta01")
+    docs("androidx.annotation:annotation-experimental:1.4.0-rc01")
     docs("androidx.appcompat:appcompat:1.7.0-alpha03")
     docs("androidx.appcompat:appcompat-resources:1.7.0-alpha03")
     docs("androidx.appsearch:appsearch:1.1.0-alpha04")
@@ -66,31 +66,31 @@
     docs("androidx.car.app:app-projected:1.4.0-rc02")
     docs("androidx.car.app:app-testing:1.4.0-rc02")
     docs("androidx.cardview:cardview:1.0.0")
-    kmpDocs("androidx.collection:collection:1.4.0-beta02")
-    docs("androidx.collection:collection-ktx:1.4.0-beta02")
-    kmpDocs("androidx.compose.animation:animation:1.6.0-beta03")
-    kmpDocs("androidx.compose.animation:animation-core:1.6.0-beta03")
-    kmpDocs("androidx.compose.animation:animation-graphics:1.6.0-beta03")
-    samples("androidx.compose.animation:animation-samples:1.6.0-beta03")
-    samples("androidx.compose.animation:animation-core-samples:1.6.0-beta03")
-    samples("androidx.compose.animation:animation-graphics-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.foundation:foundation:1.6.0-beta03")
-    kmpDocs("androidx.compose.foundation:foundation-layout:1.6.0-beta03")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.6.0-beta03")
-    samples("androidx.compose.foundation:foundation-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.material3:material3:1.2.0-beta01")
-    kmpDocs("androidx.compose.material3:material3-adaptive:1.0.0-alpha03")
+    kmpDocs("androidx.collection:collection:1.4.0-rc01")
+    docs("androidx.collection:collection-ktx:1.4.0-rc01")
+    kmpDocs("androidx.compose.animation:animation:1.6.0-rc01")
+    kmpDocs("androidx.compose.animation:animation-core:1.6.0-rc01")
+    kmpDocs("androidx.compose.animation:animation-graphics:1.6.0-rc01")
+    samples("androidx.compose.animation:animation-samples:1.6.0-rc01")
+    samples("androidx.compose.animation:animation-core-samples:1.6.0-rc01")
+    samples("androidx.compose.animation:animation-graphics-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.foundation:foundation:1.6.0-rc01")
+    kmpDocs("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.6.0-rc01")
+    samples("androidx.compose.foundation:foundation-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.material3:material3:1.2.0-beta02")
+    kmpDocs("androidx.compose.material3:material3-adaptive:1.0.0-alpha04")
     kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.0.0-alpha02")
-    samples("androidx.compose.material3:material3-adaptive-navigation-suite-samples:1.2.0-beta01")
-    samples("androidx.compose.material3:material3-adaptive-samples:1.2.0-beta01")
-    samples("androidx.compose.material3:material3-samples:1.2.0-beta01")
-    kmpDocs("androidx.compose.material3:material3-window-size-class:1.2.0-beta01")
-    samples("androidx.compose.material3:material3-window-size-class-samples:1.2.0-beta01")
-    kmpDocs("androidx.compose.material:material:1.6.0-beta03")
-    kmpDocs("androidx.compose.material:material-icons-core:1.6.0-beta03")
-    samples("androidx.compose.material:material-icons-core-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.material:material-ripple:1.6.0-beta03")
-    samples("androidx.compose.material:material-samples:1.6.0-beta03")
+    samples("androidx.compose.material3:material3-adaptive-navigation-suite-samples:1.2.0-beta02")
+    samples("androidx.compose.material3:material3-adaptive-samples:1.2.0-beta02")
+    samples("androidx.compose.material3:material3-samples:1.2.0-beta02")
+    kmpDocs("androidx.compose.material3:material3-window-size-class:1.2.0-beta02")
+    samples("androidx.compose.material3:material3-window-size-class-samples:1.2.0-beta02")
+    kmpDocs("androidx.compose.material:material:1.6.0-rc01")
+    kmpDocs("androidx.compose.material:material-icons-core:1.6.0-rc01")
+    samples("androidx.compose.material:material-icons-core-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.material:material-ripple:1.6.0-rc01")
+    samples("androidx.compose.material:material-samples:1.6.0-rc01")
     kmpDocs("androidx.compose.runtime:runtime:1.7.0-alpha01")
     docs("androidx.compose.runtime:runtime-livedata:1.7.0-alpha01")
     samples("androidx.compose.runtime:runtime-livedata-samples:1.7.0-alpha01")
@@ -102,25 +102,25 @@
     samples("androidx.compose.runtime:runtime-saveable-samples:1.7.0-alpha01")
     samples("androidx.compose.runtime:runtime-samples:1.7.0-alpha01")
     docs("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")
-    kmpDocs("androidx.compose.ui:ui:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-geometry:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-graphics:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-graphics-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-test:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-test-junit4:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-test-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-text:1.6.0-beta03")
-    docs("androidx.compose.ui:ui-text-google-fonts:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-text-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-tooling:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-tooling-data:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-tooling-preview:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-unit:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-unit-samples:1.6.0-beta03")
-    kmpDocs("androidx.compose.ui:ui-util:1.6.0-beta03")
-    docs("androidx.compose.ui:ui-viewbinding:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.6.0-beta03")
-    samples("androidx.compose.ui:ui-samples:1.6.0-beta03")
+    kmpDocs("androidx.compose.ui:ui:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-geometry:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-graphics:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-graphics-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-test:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-test-junit4:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-test-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-text:1.6.0-rc01")
+    docs("androidx.compose.ui:ui-text-google-fonts:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-text-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-tooling:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-tooling-data:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-tooling-preview:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-unit:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-unit-samples:1.6.0-rc01")
+    kmpDocs("androidx.compose.ui:ui-util:1.6.0-rc01")
+    docs("androidx.compose.ui:ui-viewbinding:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.6.0-rc01")
+    samples("androidx.compose.ui:ui-samples:1.6.0-rc01")
     docs("androidx.concurrent:concurrent-futures:1.2.0-alpha02")
     docs("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha02")
     docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha13")
@@ -128,23 +128,23 @@
     docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha13")
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.3.0-alpha02")
-    docs("androidx.core:core:1.13.0-alpha02")
+    docs("androidx.core:core:1.13.0-alpha03")
     // TODO(b/294531403): Turn on apiSince for core-animation when it releases as alpha
     docsWithoutApiSince("androidx.core:core-animation:1.0.0-rc01")
     docsWithoutApiSince("androidx.core:core-animation-testing:1.0.0-rc01")
     docs("androidx.core:core-google-shortcuts:1.2.0-alpha01")
     docs("androidx.core:core-i18n:1.0.0-alpha01")
-    docs("androidx.core:core-ktx:1.13.0-alpha02")
+    docs("androidx.core:core-ktx:1.13.0-alpha03")
     docs("androidx.core:core-location-altitude:1.0.0-alpha01")
-    docs("androidx.core:core-performance:1.0.0-rc01")
-    docs("androidx.core:core-performance-play-services:1.0.0-rc01")
-    samples("androidx.core:core-performance-samples:1.0.0-rc01")
-    docs("androidx.core:core-performance-testing:1.0.0-rc01")
+    docs("androidx.core:core-performance:1.0.0")
+    docs("androidx.core:core-performance-play-services:1.0.0")
+    samples("androidx.core:core-performance-samples:1.0.0")
+    docs("androidx.core:core-performance-testing:1.0.0")
     docs("androidx.core:core-remoteviews:1.0.0-rc01")
     docs("androidx.core:core-role:1.2.0-alpha01")
     docs("androidx.core:core-splashscreen:1.1.0-alpha02")
     docs("androidx.core:core-telecom:1.0.0-alpha02")
-    docs("androidx.core:core-testing:1.13.0-alpha02")
+    docs("androidx.core:core-testing:1.13.0-alpha03")
     docs("androidx.core.uwb:uwb:1.0.0-alpha07")
     docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha07")
     docs("androidx.credentials:credentials:1.3.0-alpha01")
@@ -155,15 +155,15 @@
     docs("androidx.customview:customview:1.2.0-alpha02")
     // TODO(b/294531403): Turn on apiSince for customview-poolingcontainer when it releases as alpha
     docsWithoutApiSince("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
-    kmpDocs("androidx.datastore:datastore:1.1.0-alpha07")
-    kmpDocs("androidx.datastore:datastore-core:1.1.0-alpha07")
-    kmpDocs("androidx.datastore:datastore-core-okio:1.1.0-alpha07")
-    kmpDocs("androidx.datastore:datastore-preferences:1.1.0-alpha07")
-    kmpDocs("androidx.datastore:datastore-preferences-core:1.1.0-alpha07")
-    docs("androidx.datastore:datastore-preferences-rxjava2:1.1.0-alpha07")
-    docs("androidx.datastore:datastore-preferences-rxjava3:1.1.0-alpha07")
-    docs("androidx.datastore:datastore-rxjava2:1.1.0-alpha07")
-    docs("androidx.datastore:datastore-rxjava3:1.1.0-alpha07")
+    kmpDocs("androidx.datastore:datastore:1.1.0-beta01")
+    kmpDocs("androidx.datastore:datastore-core:1.1.0-beta01")
+    kmpDocs("androidx.datastore:datastore-core-okio:1.1.0-beta01")
+    kmpDocs("androidx.datastore:datastore-preferences:1.1.0-beta01")
+    kmpDocs("androidx.datastore:datastore-preferences-core:1.1.0-beta01")
+    docs("androidx.datastore:datastore-preferences-rxjava2:1.1.0-beta01")
+    docs("androidx.datastore:datastore-preferences-rxjava3:1.1.0-beta01")
+    docs("androidx.datastore:datastore-rxjava2:1.1.0-beta01")
+    docs("androidx.datastore:datastore-rxjava3:1.1.0-beta01")
     docs("androidx.documentfile:documentfile:1.1.0-alpha01")
     docs("androidx.draganddrop:draganddrop:1.0.0")
     docs("androidx.drawerlayout:drawerlayout:1.2.0")
@@ -180,9 +180,9 @@
     docs("androidx.enterprise:enterprise-feedback:1.1.0")
     docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
     docs("androidx.exifinterface:exifinterface:1.3.6")
-    docs("androidx.fragment:fragment:1.7.0-alpha07")
-    docs("androidx.fragment:fragment-ktx:1.7.0-alpha07")
-    docs("androidx.fragment:fragment-testing:1.7.0-alpha07")
+    docs("androidx.fragment:fragment:1.7.0-alpha08")
+    docs("androidx.fragment:fragment-ktx:1.7.0-alpha08")
+    docs("androidx.fragment:fragment-testing:1.7.0-alpha08")
     docs("androidx.glance:glance:1.0.0")
     docs("androidx.glance:glance-appwidget:1.0.0")
     samples("androidx.glance:glance-appwidget-samples:1.0.0")
@@ -195,11 +195,11 @@
     docs("androidx.glance:glance-wear-tiles-preview:1.0.0-alpha06")
     docs("androidx.graphics:graphics-core:1.0.0-beta01")
     samples("androidx.graphics:graphics-core-samples:1.0.0-beta01")
-    docs("androidx.graphics:graphics-path:1.0.0-beta01")
+    docs("androidx.graphics:graphics-path:1.0.0-beta02")
     docs("androidx.graphics:graphics-shapes:1.0.0-alpha04")
     docs("androidx.gridlayout:gridlayout:1.1.0-beta01")
-    docs("androidx.health.connect:connect-client:1.1.0-alpha06")
-    samples("androidx.health.connect:connect-client-samples:1.1.0-alpha06")
+    docs("androidx.health.connect:connect-client:1.1.0-alpha07")
+    samples("androidx.health.connect:connect-client-samples:1.1.0-alpha07")
     docs("androidx.health:health-services-client:1.1.0-alpha02")
     docs("androidx.heifwriter:heifwriter:1.1.0-alpha02")
     docs("androidx.hilt:hilt-common:1.2.0-alpha01")
@@ -216,34 +216,34 @@
     docs("androidx.leanback:leanback-paging:1.1.0-alpha11")
     docs("androidx.leanback:leanback-preference:1.2.0-alpha04")
     docs("androidx.leanback:leanback-tab:1.1.0-beta01")
-    docs("androidx.lifecycle:lifecycle-common:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-common-java8:2.7.0-rc02")
+    docs("androidx.lifecycle:lifecycle-common:2.7.0")
+    docs("androidx.lifecycle:lifecycle-common-java8:2.7.0")
     docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
-    docs("androidx.lifecycle:lifecycle-livedata:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-livedata-core:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-process:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-reactivestreams:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-runtime:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-runtime-compose:2.7.0-rc02")
-    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-runtime-testing:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-service:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-viewmodel:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0-rc02")
-    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0-rc02")
-    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0-rc02")
+    docs("androidx.lifecycle:lifecycle-livedata:2.7.0")
+    docs("androidx.lifecycle:lifecycle-livedata-core:2.7.0")
+    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0")
+    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
+    docs("androidx.lifecycle:lifecycle-process:2.7.0")
+    docs("androidx.lifecycle:lifecycle-reactivestreams:2.7.0")
+    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0")
+    docs("androidx.lifecycle:lifecycle-runtime:2.7.0")
+    docs("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
+    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.7.0")
+    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+    docs("androidx.lifecycle:lifecycle-runtime-testing:2.7.0")
+    docs("androidx.lifecycle:lifecycle-service:2.7.0")
+    docs("androidx.lifecycle:lifecycle-viewmodel:2.7.0")
+    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
+    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.7.0")
+    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
+    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0")
     docs("androidx.loader:loader:1.1.0")
     // localbroadcastmanager is deprecated
     docsWithoutApiSince("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
-    docs("androidx.media2:media2-common:1.3.0-rc01")
-    docs("androidx.media2:media2-player:1.3.0-rc01")
-    docs("androidx.media2:media2-session:1.3.0-rc01")
-    docs("androidx.media2:media2-widget:1.3.0-rc01")
+    docs("androidx.media2:media2-common:1.3.0")
+    docs("androidx.media2:media2-player:1.3.0")
+    docs("androidx.media2:media2-session:1.3.0")
+    docs("androidx.media2:media2-widget:1.3.0")
     docs("androidx.media:media:1.7.0")
     // androidx.media3 is not hosted in androidx
     docsWithoutApiSince("androidx.media3:media3-cast:1.2.0")
@@ -273,7 +273,7 @@
     docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.2.0")
     docs("androidx.mediarouter:mediarouter:1.7.0-alpha01")
     docs("androidx.mediarouter:mediarouter-testing:1.7.0-alpha01")
-    docs("androidx.metrics:metrics-performance:1.0.0-alpha04")
+    docs("androidx.metrics:metrics-performance:1.0.0-beta01")
     docs("androidx.navigation:navigation-common:2.7.6")
     docs("androidx.navigation:navigation-common-ktx:2.7.6")
     docs("androidx.navigation:navigation-compose:2.7.6")
@@ -382,8 +382,8 @@
     // TODO(243405142) clean-up
     docsWithoutApiSince("androidx.tracing:tracing-perfetto-common:1.0.0-alpha16")
     docs("androidx.tracing:tracing-perfetto-handshake:1.0.0")
-    docs("androidx.transition:transition:1.5.0-alpha05")
-    docs("androidx.transition:transition-ktx:1.5.0-alpha05")
+    docs("androidx.transition:transition:1.5.0-alpha06")
+    docs("androidx.transition:transition-ktx:1.5.0-alpha06")
     docs("androidx.tv:tv-foundation:1.0.0-alpha10")
     docs("androidx.tv:tv-material:1.0.0-alpha10")
     samples("androidx.tv:tv-samples:1.0.0-alpha10")
@@ -391,31 +391,31 @@
     docs("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
     docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
     docs("androidx.vectordrawable:vectordrawable-seekable:1.0.0-beta01")
-    docs("androidx.versionedparcelable:versionedparcelable:1.2.0-rc01")
+    docs("androidx.versionedparcelable:versionedparcelable:1.2.0")
     docs("androidx.viewpager2:viewpager2:1.1.0-beta02")
     docs("androidx.viewpager:viewpager:1.1.0-alpha01")
-    docs("androidx.wear.compose:compose-foundation:1.3.0-beta02")
-    samples("androidx.wear.compose:compose-foundation-samples:1.3.0-beta02")
-    docs("androidx.wear.compose:compose-material:1.3.0-beta02")
-    docs("androidx.wear.compose:compose-material-core:1.3.0-beta02")
-    samples("androidx.wear.compose:compose-material-samples:1.3.0-beta02")
+    docs("androidx.wear.compose:compose-foundation:1.3.0-rc01")
+    samples("androidx.wear.compose:compose-foundation-samples:1.3.0-rc01")
+    docs("androidx.wear.compose:compose-material:1.3.0-rc01")
+    docs("androidx.wear.compose:compose-material-core:1.3.0-rc01")
+    samples("androidx.wear.compose:compose-material-samples:1.3.0-rc01")
     docs("androidx.wear.compose:compose-material3:1.0.0-alpha16")
-    samples("androidx.wear.compose:compose-material3-samples:1.3.0-beta02")
-    docs("androidx.wear.compose:compose-navigation:1.3.0-beta02")
-    samples("androidx.wear.compose:compose-navigation-samples:1.3.0-beta02")
-    docs("androidx.wear.compose:compose-ui-tooling:1.3.0-beta02")
-    docs("androidx.wear.protolayout:protolayout:1.1.0-alpha04")
-    docs("androidx.wear.protolayout:protolayout-expression:1.1.0-alpha04")
-    docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.1.0-alpha04")
-    docs("androidx.wear.protolayout:protolayout-material:1.1.0-alpha04")
-    docs("androidx.wear.protolayout:protolayout-material-core:1.1.0-alpha04")
-    docs("androidx.wear.protolayout:protolayout-renderer:1.1.0-alpha04")
-    docs("androidx.wear.tiles:tiles:1.3.0-alpha04")
-    docs("androidx.wear.tiles:tiles-material:1.3.0-alpha04")
-    docs("androidx.wear.tiles:tiles-renderer:1.3.0-alpha04")
-    docs("androidx.wear.tiles:tiles-testing:1.3.0-alpha04")
-    docs("androidx.wear.tiles:tiles-tooling:1.3.0-alpha04")
-    docs("androidx.wear.tiles:tiles-tooling-preview:1.3.0-alpha04")
+    samples("androidx.wear.compose:compose-material3-samples:1.3.0-rc01")
+    docs("androidx.wear.compose:compose-navigation:1.3.0-rc01")
+    samples("androidx.wear.compose:compose-navigation-samples:1.3.0-rc01")
+    docs("androidx.wear.compose:compose-ui-tooling:1.3.0-rc01")
+    docs("androidx.wear.protolayout:protolayout:1.1.0-beta01")
+    docs("androidx.wear.protolayout:protolayout-expression:1.1.0-beta01")
+    docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.1.0-beta01")
+    docs("androidx.wear.protolayout:protolayout-material:1.1.0-beta01")
+    docs("androidx.wear.protolayout:protolayout-material-core:1.1.0-beta01")
+    docs("androidx.wear.protolayout:protolayout-renderer:1.1.0-beta01")
+    docs("androidx.wear.tiles:tiles:1.3.0-beta01")
+    docs("androidx.wear.tiles:tiles-material:1.3.0-beta01")
+    docs("androidx.wear.tiles:tiles-renderer:1.3.0-beta01")
+    docs("androidx.wear.tiles:tiles-testing:1.3.0-beta01")
+    docs("androidx.wear.tiles:tiles-tooling:1.3.0-beta01")
+    docs("androidx.wear.tiles:tiles-tooling-preview:1.3.0-beta01")
     docs("androidx.wear.watchface:watchface:1.2.0")
     docs("androidx.wear.watchface:watchface-client:1.2.0")
     docs("androidx.wear.watchface:watchface-client-guava:1.2.0")
@@ -438,10 +438,12 @@
     samples("androidx.wear:wear-input-samples:1.2.0-alpha01")
     docs("androidx.wear:wear-input-testing:1.2.0-alpha02")
     docs("androidx.wear:wear-ongoing:1.1.0-alpha01")
-    docs("androidx.wear:wear-phone-interactions:1.1.0-alpha03")
-    docs("androidx.wear:wear-remote-interactions:1.1.0-alpha01")
+    docs("androidx.wear:wear-phone-interactions:1.1.0-alpha04")
+    samples("androidx.wear:wear-phone-interactions-samples:1.1.0-alpha04")
+    docs("androidx.wear:wear-remote-interactions:1.1.0-alpha02")
+    samples("androidx.wear:wear-remote-interactions-samples:1.1.0-alpha02")
     docs("androidx.wear:wear-tooling-preview:1.0.0")
-    docs("androidx.webkit:webkit:1.10.0-beta01")
+    docs("androidx.webkit:webkit:1.10.0-rc01")
     docs("androidx.window.extensions.core:core:1.0.0")
     docs("androidx.window:window:1.3.0-alpha01")
     stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index 9e8b4e4..e449520 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -19,7 +19,6 @@
 import android.app.AlertDialog
 import android.app.Dialog
 import android.content.DialogInterface
-import android.os.Build
 import android.os.Bundle
 import android.os.Looper
 import androidx.fragment.app.test.EmptyFragmentTestActivity
@@ -112,12 +111,6 @@
 
     @Test
     fun testDialogFragmentDismiss() {
-        // There is a leak in API 30 InputMethodManager that causes this test to be flaky.
-        // Once https://github.com/square/leakcanary/issues/2592 is addressed we can upgrade
-        // leak canary and remove this.
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
-            return
-        }
         val fragment = TestDialogFragment()
         activityTestRule.runOnUiThread {
             fragment.showNow(activityTestRule.activity.supportFragmentManager, null)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index ec9f1a4..8060409 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -20,7 +20,6 @@
 import android.app.Dialog
 import android.content.Context
 import android.content.DialogInterface
-import android.os.Build
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -534,12 +533,6 @@
 
     @Test
     fun testRequireDialog() {
-        // There is a leak in API 30 InputMethodManager that causes this test to be flaky.
-        // Once https://github.com/square/leakcanary/issues/2592 is addressed we can upgrade
-        // leak canary and remove this.
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
-            return
-        }
         val dialogFragment = TestDialogFragment()
         val fm = activityTestRule.activity.supportFragmentManager
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ba75ff3..a66c24c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -49,7 +49,7 @@
 ksp = "1.9.21-1.0.15"
 ktfmt = "0.45"
 ktlint = "0.49.1"
-leakcanary = "2.12"
+leakcanary = "2.13"
 media3 = "1.1.0"
 metalava = "1.0.0-alpha10"
 mockito = "2.25.0"
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 24c39b0..35cbf8c 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -8897,6 +8897,201 @@
 =ajY9
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    7721F63BD38B4796
+sub    4EB27DB2A3B88B8B
+sub    1397BC53640DB551
+sub    78BD65473CB3BD13
+sub    6494C6D6997C215E
+sub    E88979FB9B30ACF2
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx
+BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS
+pJT+0m2SgUNhLAn1WY/iNJGNaMl7lgUnaP+/ZsSNT9hyTBiH3Ev5VvAtMGhVI/u8
+P0EtTjXp4o2U+VqFTBGmZ6PJVhCFjZUeRByloHw8dGOshfXKgriebpioHvU8iQ2U
+GV3WNIirB2Rq1wkKxXJ/9Iw+4l5m4GmXMs7n3XaYQoBj28H86YA1cYWSm5LR5iU2
+TneI1fJ3vwF2vpSXVBUUDk67PZhg6ZwGRT7GFWskC0z8PsWd5jwK20mA8EVKq0vN
+BFmMK6i4fJU+ux17Rgvnc9tDSCzFZ1/4f43EZ41uTmmNXIDsaPCqwjvSS5ICadt2
+xeqTWDlzONUpOs5yBjF1cfJSdVxsfshvln2JXUwgIdKl4DLbZybuNFXnPffNLb2v
+PtRJHO48O2UbeXS8n27PcuMoLRd7+r7TsqG2vBH4t/cB/1vsvWMbqnQlaJ5VsjeW
+Tp8Gv9FJiKuU8PKiWsF4EGR/kAFyCB8QbJeQ6HrOT0CXLOaYHRu2TvJ4taY9doXn
+98TgU03XTLcYoSp49cdkkis4K+9hd2dUqARVCG7UVd9PY60VVCKi47BVKQARAQAB
+uQINBGF4DJ8BEACk2Gwau+s/pKmOTnGLMnB3ybQsiVGLRhsw2SqSTvSyBthAyW1U
+AqdRqNA8/FdMlvVuppG8+vCLXPmpP63C+9M2tyQeOR2aVQp+u1EIwN4lPu4wrh6v
+dtgSRim8uxBdLIHG16z0xxVhE2rM/Ot/gucfkpoEw289VaR7sPmIxfVTm1QcqCGi
+FQl3rZnma6Bz8UOXJoE8wO+LK5WkcdmFz6+Z3BLSb5IL9lhsArFToNq5dN2SSTbC
+TdHRzrRuoCdefYHdxoLCM4kJfggRRgWhKoEJro+ZipESq1T5yHV/iAJy+3DuC8Lb
+YLvsjt9VZYARw8xIGb90Vj3ThWuMoVr/IVmKT7foC5Whe0PTI/b2frNaWCxxC4cR
+VxMusiBX66mclQ4Mvzwj50G1WKygULYcvPQ81Tg0pvgTKqgxwL9luN9MiDVtkn9C
+Zx7NFlszVr+ic7nVJjANnJebFHCEZfJbQo4uIwKfYbhopUkCa41iXpesbVzAKqNw
+ePgyNTAMFyYnjAUE8FVUmx7ZJVb15iEbMs38gJKJ/Wb8wtJRflAfkhrEzh1M/43W
+UAU3RfPmXTrGeyDCYKTHiXTnj748uH6U40sB9q+qeEhZdTj0KufjgtWaFWsZTkVr
+tGOaI6xfX6py/k3hjU3es+7ddElxhPBcqNE3pkPRqb9wz+exSdM7hiUzNwARAQAB
+iQRbBBgBCAAPBQJheAyfAhsCBQkFo5qAAkAJEHch9jvTi0eWwV0gBBkBCAAGBQJh
+eAyfAAoJEE6yfbKjuIuLggkP/1INRyRToLmY1ms9DTWMQ0lwbBL8J3xu/neKIOKV
+GOdw9zcWlGugUoOthSbT8bjvuybH1Vjx4wFM+cnuMVfjD58Xu6ZpgCHN1wXYMuzY
+weBFKaMg4oSwTKuAJBJ2IhfEm/cAryVvKY2zY+uyzgizx3vAg3sjkAPDcrSCJP2n
+kuHcJ3nzUbKNAjmdMsnWDrqqZVwP99nuyMk8bAtueZ0SKvIpCv2wIeYO7zkj61vu
+QOFOGhl98OBui5wUhtgQw//esTWYiGNKSmD3derd2JHVA01tBmCWV4KMLDbg3CcM
+MQ1x3V1me6EG3giwBL1I9xTsBUbEa6eEN9U0zdKvoMbSogON5wCuxAzO/CXGMreJ
+tBUupHEc69oTuwe426Ihi3AbRrPAg3tnGGFCt11HoQFNnRPWb3unF8UlA2rSytvw
+FyQi3pzBYt5VsTIA7NEHGuJs+/Oor6AOInzht1cp7AfmDGfGy2N5ow+4GI6FPe2U
+qIg2+nFiGr9hRZOvXRgLQL8dlDnFChymldxm/J/UFdJGSWRldEDsPrzHQESKvsV9
+EjnJQR5p5zkQK6jx0zqSlDgiNG2GT3/CSvwIdCih6Cl9HThHtYNm3ZYN0bU9W2je
+oLh3AINNTcrp0tAHZuQLFxukbj56O5eB+nfk67/X2iNii46ZdJQNwbT9YN6CstQz
++CnqFiEE60wb/U8EL23dzOyRdyH2O9OLR5aDtg//cbpjo0chCCBeeVgiLeLA3vaE
+SASrPq8hErzuUEZbavd5DRwNm4Tf7lDgVhyLD4HZEp4OGN2Y8fKkDmj5GIDIjsk5
+nAlqWoc7efAkbmyvStHNwmxsa+lvOyjYm5PJNRG/i0E2rjlv3LRB3O3k+k2s8ltA
+AMlaf4daxtUkHmBYFN2hBiCnJOvzidDKxxYBQVNFuYe+2MIJ8t29TzAzu5sBDkPC
+LWkAFG21EAy48D3gfNoEXnJeSCHEemdbQhxcaLCByH0tDJo71VJGGI8fqvlm6Tsq
+8aEemHtILkmBSf28maanXNx3SZdDZmHwzzUndGLeIY8czYKqmUDFc1siufO1sQmE
+3Yj7vubvdnh34rWK+DrFCG15JmHCHHv9ndOX5TaNg4QUif9QWZXdTFFIlr+NBFyO
+8wqmtKni0BnbIkjdtpuNFuLGBQS2UrXTn8l6nFwB3D2+izE7+tHtWoLO7Ryil3EL
+QYAfyiD4D3/cs+GVEbLdCF/OPL0kdYePgQiyiXTYFLz51a8Chh6uS970736Hr8Hn
+Az9ieD0GNP46s2+R+aorZyykFfBh506sLi5ZxSa54RWu/k/gUXfrAn56O89Lq91P
+FVN0teOi5QfBNBBlWU2NZjdwjKPTednX1z5vfT7YXMb+5Kdv949axEtjsjLPjKCv
+x63B4E+cQi+PCkBnE665Ag0EVwyNyAEQAKsv2AeF2vqBBfhkwDmyWnrbzE6scKx0
+s7nhY109Ep4UdcmpJImLd+zwXEFYjgWd6N4pQZsX4ys6UWkqoQvFoyN7tvBnJqne
+LPO1kezM/diY6hMEm9EQYp0KQvzZwuwKFgP8+uATxyu+SFKer169ywoCfOIzGD/A
+MIKFQvcS+qjb0F6gHzV/4T3CStRMwJP+RXG3ekZFqUpfRSGu0qumbzJF+O58l/CO
+R3CC+KeREZnYatYePgvMxuL3+51holnrpjDSERThRLFQH2822ZIWtvgQH3VPauFz
+rx2BDiNgEjsrgRtvxdpYDFv4gCrfWXVSSIQDfYXipQygvqsKEHjLqcfE6dO+z5cR
+vlMHBdWiCMtEpNCzlT8dX2XuP4cByGTnLeKbY3ZQqYzEeqi289llRk91oJHFR51B
+/2BHTItlX5T0FwO7CPMv/OOu2E1liUQYnodn9MtJOnh0Mf65e4uoxVbLmKq4q2du
+uc1NC2/m3AP4COmDLrRgs4n1hqIngaOJ86nNKTzd7Wsnen+lfoHk1ZCKdUtknPHJ
+46iHeIyN2YINKcRcusKZi/mDqPJX9Zt3gZgW4wrxNPv49B1Ytxtn8vFznDSz5zv5
+/k5+Ypc7ko8eedSysXkMFopE+NJynB49CK3F4iCVSAQwOQ2u4GG7U/MLF3cG1eC7
+74rdZ2gfdVyDABEBAAGJBFsEGAECAA8FAlcMjcgCGwIFCQWjmoACQAkQdyH2O9OL
+R5bBXSAEGQECAAYFAlcMjcgACgkQE5e8U2QNtVFBJg//QTCvdPt7SyhPPyDhAkst
+WpkNl1fwh7PTiJ00e68C7QDB1nbCXQL60yQPuXhHZojoEp7/3A+d2T80l75lhwP+
+7PKIoglAPjw+uJ82fC8e70DzSsTgGmlCemUQ16GJttZoY0lA40YUnHtBNiUWNLks
+2UbUBfqZCPG9vjbfM5ZI6YRqZhdgGZjIwbq+Sv9dM/OyV2TLxcW4+slRmyUv9aXH
+fVdDUiu2Qcc5ipbCvSFNznT/Y7wfR7CX90FkurcSaKdln62xO6Ch/SPhJvFiGmXD
+32cbBs3W5fLgvz91Y5Redjk6BpMpk8XXnNEzFc30V7KUFVimnmTOt7+tEjqZDaVp
+9gd1uO93uvIcXkm9hOhINd3SbMXacvObqPCw7zjtk13kZ1MPr+9x5/Ugm1rWdLAD
++GEu2C2XPr+02dyneUR0KMAzHb2Ng8Nf4uqz0kDFwke5+vzajrAz1MXbhDytrw1u
+8Hreh1WJ0J+Ieg6wgUNStrMfxe5pDPJmQjRtvMuaAwC8w7q7XM9979Mrot0mDsB4
+ApJw4lLfwPmabBoPVsAGvrt5sD9fkd1qiZIMpV1Rhp7B9MYEiytaYKYql1v5Z9fi
+h0Wk3Ndb+qySIGnlZJ6wq83VBSQslkNkPWTPb75e6XkH3uzkvEtMtHC+Aug1pQWv
+eWd6PM0uB0Gl/oWeQDn2zJEWIQTrTBv9TwQvbd3M7JF3IfY704tHllLiD/44gGdr
+ic11Sj3kSajBi/lHth4lRHvIpmt0kCHHqlvIyku3OzYnOnTFsYJw6ZObSZlY64H3
+NGvXNRWOBqJlAW2RY/o+9/lkZNxGlSiLLcvsc+PPq5SY3WBVQiaNCaeu4441zreh
+Dh054jgPwDbc0SMwzJ1q8CWewMQhieGIy0oZHETPUt9F9IrgJMTSC1/vkjTnfGqn
+NmxaH8kwpHG5OgiHNVtNL3379UBkzY7LTaVUpffm0ZpSQ6iVpwxpOTZeDXKnfF+d
+VdLWBIOlhCuDjPN8MeRMP03U0auYyZGwrthmDWjTTYF1u210r0+VSDW7j+nZm4tf
+/LNIyiUDhASN0ccaAftNQ4MfWHTX47AjN4b0L8pGljAC3mSKc218VddfGPUEFCEX
+ti/uF2QAhAIVo49itcD+zgxk0K6zOKuLCMNZd5e8jvWl1ze5YpAAHXU+FhKRldAo
+qK0Xjidcv7CiUSUiP56TdVCu8DGs2fss4cTdKsxvLKkRCiUJzbrhkEP6iBm/6HuH
+TztBYoQFBpFqGtG4LRlYozeX0EmzG1edJGWKmmvRpzUEZ3IZmW/qStsS3CvOYQ9Z
+6d+/wJuaNoIKDjY96iNyWkR7GWjrNaj0HMneOjsiBVKRySH5OJ3swwqEGHfd21lB
+qg8MSP5/wrHTtN+ucqgzvwA7HHoZVanQLqVb9rkCDQRdNfyuARAArJGS9PHK6yn1
+9mg9sPDlEt41RkluytdCa/3YJemQe18f8t3PZYQYpu6ESLJeraUfwwT+dFJ0yIAW
+78p2yerW1hqbbeuAM5yO3pfaR9JOtwzinH6kVICSkzODlGUpHLqkNqp5L5/oNKIi
+d1YWlWDiXSXyRf6S4+Tot1BC9mn6BHdOPP5WRGgE0bxKi4/odqUnxExJecAlskZ7
+jErumsvTZlc0cYiYTwxltqaUE8Sbmio/ulasOViErvZ7EcUzxNQ1vH+y2MjwcILp
+bbkmCmaAnX6GBPH3+O9aGSLginBkkgR0G0ejAUw+iibaswc6XyfM2SZlTuJYNAiY
+KfcV9kGfIT5KWgW56tiQjEtc64gjDfIhoVumR2JmUCbVrrm7mbedVS/UJhFFIQhd
+bft5XddUmqMGODRSiC1HWGwtswDkCBBCZFkSBypXrnNs2EjUqF5dOqBoMAikG+Zf
+i5XGB8ePvxd/is8gKtDagQtazJ4W597XHJHWcoSxKZ8ff8t98qaOmWdLOnSxq5kq
+SaXnRM+s6/LIgWuLk9yL1ReMY36c0rd3DA/y84W55OaJ1H5JqApkP6ZLiqTK9+YR
+7XQ8bIS3CzxZE53DIxFQDgU6Bw2yg0qZbnsiZuPBCz4vWZrsjbu1rAfE1lRAJFEo
+cXXhwC1aWNQ/jI1yvmeWqev7aaY/n5sAEQEAAYkEWwQYAQgADwUCXTX8rgIbAgUJ
+BaOagAJACRB3IfY704tHlsFdIAQZAQgABgUCXTX8rgAKCRB4vWVHPLO9E81AD/9U
+KuvTNQjHd9NkzJ6dWIdTtOsJAjxRpzBTFcGEVx+YIKGFG9yKAjpiABzhJ+r/X4aN
+K2CJkvqEQxXhNPHTsXiPgqNMVYrFRydSSb3Z7ttegWlxLM7WdChslJRYnV0Tq8J3
+ZaRN6pc+LdcIfxBGlB9k2kI0SfjtZlybRerywdfmLzEzM9/v92nl/3pQ8/WmrR0q
+YwVUtjc2x7q2/YO5qg+OZwfNMkwbD2E++N/TvZu9DqrMY6A6ntGy/15SF7DPcN3o
+v1LO60n0mOc4CCn+/4rvmiXDAEAvWYq+1wElFbGwWcsH6aNr+PwAlghyet5RUk9k
+K5X+EY49T4RdIn7wdlzEeknsDZA4FZUFfmETztV8EzppaBVPKnqZLfJ5tEDNZYXT
+kHSlXtEMjZ+m7clAiiO6eKSnPP8n2ZuY+hG6F4hgG91wnEhYUeqRa2nddmDHCgYR
+WLywmgjeoNaubEg8b8an0RE7xBVcKjs5X4lB/SlWN1iXDmdwqW3rt0AqT5E4lWYG
+50fllPSUhkBRIu4mU60/LXLeISINT2t+QdZRmSJaWJs73TeWKFfaISaGtQcY+6om
+pU0/0rtRG/7aDjpDpakOwftTQV87/RZogZ7QuGn8pByHhCBEjV/S3ZrwfpEDFu3d
+XewbNGltqJDw7dNr4MB9lRLUE7X6uQffI28DVvZm1xYhBOtMG/1PBC9t3czskXch
+9jvTi0eWTvMQAKKCKl+H8T72WdueqgPKHEkXDZtJmTn6nyneYlETvdmHGEIb1ejx
+uJ5URlAYnciY+kvSQ/boKjVHNGmf6+JBexd+HqPhkeextV6Jcnmi47HDvIU/TSyn
+huqZeK/3SZAV7ESqQl42q7wm7Pqw0dkv4jjFCRxDA+Qq2aH6szJ7DZxTRWqfR3Zb
+e78NyFVXKxhFQO72zHzC3pFu/Ak59hmTU23yoXVo5t+5O+Q21kX2dbuLd6Px1bnT
++EmyneoPP1Emea5jgsw2/ECqHnvNt6cbp+42XYldGh+PBHBmucC3Mn7sALajHe5k
+2XkNlfbjSNlmutxQFH1qq9rh/JVyxJNHeGzV5G0timAwfdJFUzE1vNU5P0w4O8Hr
+CsX5Ecfgcw2BQ9vPCE3OfG+11xp6oiNMRVsR5pTu7RiI1BQAyICWUW/wXuhhHkkw
+NTiwfciJfVA8ckOiRubik8geEH5boOxgeAaBu6yusQVHnRRyG4wjQ+qsWo+wDI9W
+MdtpNG1toJrSUL4OYa4oX3YogSv5hGrbYIaP4HwO6O2oTMnS0lRIGJOqbEQcmKUa
+/nWT/3NipTnYzyMjMlEQe89YKjd+32tjMfOSdIOvwCGaTizdWnKPF77qB9D0v8C/
+7AdHmEFqf2ZX8vK31aaY+ZpPWG5IHlf6f/buIMBalJOxIBeveBqxcHwQuQINBFiG
+v8wBEACtrmK7c12DfxkPAJSD12VanxLLvvjYW0KEWKxN6TMRQCawLhGwFf7FLNpa
+b829DFMhBcNVgJ8aU0YIIu9fHroIaGi+bkBkDkSWEhSTlYa6ISfBn6Zk9AGBWB/S
+IelOncuAcI/Ik6BdDzIXnDN7cXsMgV1ql7jIbdbsdX63wZEFwqbaiL1GWd4BUKhj
+0H46ZTEVBLl0MfHNlYl+X3ib9WpRS6iBAGOWs8Kqw5xVE7oJm9DDXXWOdPUE8/FV
+ti+bmOz+ICwQETY9I2EmyNXyUG3iaKs07VAf7SPHhgyBEkMngt5ZGcH4gs1m2l/H
+FQ0StNFNhXuzlHvQhDzd9M1nqpstEe+f8AZMgyNnM+uGHJq9VVtaNnwtMDastvNk
+UOs+auMXbNwsl5y/O6ZPX5I5IvJmUhbSh0UOguGPJKUu/bl65theahz4HGBA0Q5n
+zgNLXVmU6aic143iixxMk+/qA59I6KelgWGj9QBPAHU68//J4dPFtlsRKZ7vI0vD
+14wnMvaJFv6tyTSgNdWsQOCWi+n16rGfMx1LNZTO1bO6TE6+ZLuvOchGJTYP4LbC
+eWLL8qDbdfz3oSKHUpyalELJljzin6r3qoA3TqvoGK5OWrFozuhWrWt3tIto53oJ
+34vJCsRZ0qvKDn9PQX9r3o56hKhn8G9z/X5tNlfrzeSYikWQcQARAQABiQRbBBgB
+AgAPBQJYhr/MAhsCBQkFo5qAAkAJEHch9jvTi0eWwV0gBBkBAgAGBQJYhr/MAAoJ
+EGSUxtaZfCFeW4kP/iZq+blRDzgRzOw16x80vyBjfPOUKd++dSUkcr4Khi5vjByg
+NdVSWcKZaBKVkdBmCvf+p9bYwzfL+RdxvGEv8WKNTNjdaWcJ2chU2O4H5Am3Qsdu
+Q/sSf+jTzlnMe7NpfF9n3uo34o+xEFOOcnyF3cHrhxWOCde9rX6kbnUQriIMXZte
+JY8e9Rs+Iv46DoL1eOlavAgDUJbIf/iLt219OdtWI7ZqopA0d+tcn7FL3fwuvyvn
+5WZRYHIerB4EYgBI6bCwl5JQejORlhuYx1oknyPjnzPJ9Los74chrf7OHOJ06iIQ
+f1zlC9V/niA2xiM9NwePtTQOCTEJVB6IEoEtH6rozpAdriprH9fRnZkJxINNnCoY
+k1op9wVh3xfUHbOCvGQbB54cqN+amp9dEquCAe6Yt1WodTspL1zPXJ5Mv43Dud76
+TNEwQDywuebg4NFQnBTPXZGpLQYbUVhXSuMlVZXNEUx8xSz7vECm0S4x2h12RBKb
+K2RfI4oCq/wpD1dQRsZaKSYLFbZw5j2yk6nBBrtfahd7sWVX1F+YdisbTeT5iUhE
+SAWqW9bCyCnNRFy6V34IgW9Pe9yLu8WbVSJAFvnALxsc6hGyvs5dbXbruWKmi5mv
+k6tCFWdFlBVrrhx1QgqMtcS3jv3S7GHyCA3CS1lEgsifYkeOARAgJ1hZ5BvUFiEE
+60wb/U8EL23dzOyRdyH2O9OLR5a6tQ/7BvrqUiEMHRT024V0lRNzqc7/VzLeL2YY
+JWirCZNwHUEn0TWGdwxQ/X8y+sCCQEGsBYmIPZeP1BX+4GjMDucvGtXtx/V1704u
+Afh40R9fbnGPwEE9ifJXIezKTlkcOGOP1Jq6t++UVZiLXeLmJ4ynItUlXj6vZaLX
+HVSApUCUHvywdidsPNKgc8sFwjG0gqnfQsnQ8SRq3xIbsbbCC2/VF2GZxkQFXTm8
+yesYY733V76krNE09gfysPdVAomW9oXCwm4FNbjaBncp+lQ0oja6AJydE2mgcVY0
+KvOUnQIUjjwsyWZRKz7uL+X5AaV5ospDFFM02kCZ68utLuBrgirQgSrF7LpVbpFK
+T9nVnfsnwHs8hJJHStmfpN7JCAVZ/8U4vDo63A6q4aYx4ZKqPZTPHJvKC2y9RnPQ
+HFjKahYV3gXyynjegvZNdTpantAikFoWCRJ/9RDF9HrBxDeK9p95LWWDy9QW4x+4
+wu2jpPQmJnDy4pK4NaW1JF4X4VdWeFrDPvYFkzLxEiiyEZ2lYef9hs3ov4k67Fme
++QwBm9ItPdzQfhcINi0R8raYkyoPbKdfm0ZHwXLcYBoSOHHfpO/NOMJdGYn4WeVJ
+bWnvvH5SMVQVyljyyS/5/QaKnfSLKcBvWGgB2JsgNpvrXY5aL/i9fQNozq3rxSry
+uxpt4lixkbi5Ag0EY+z5WAEQAMpuAsv68BpspXasZIOBj4i21MC2D3D1SivX5D8I
+hYbGL8lDLuXtdMqOZLTC93BT71/94ayPGKBu8BxKxXWzPIp9VBAVPab0qEOsGo0I
+C9DlODplB017nWgfCOIhDWLvPxChPbZW5x1NipwwsAd76DRo2ostJJYsCJbjP6d6
+tyWKeOJNELD9pZklHWjNB3Z4rADDquTX1sdyKFejYTQrUQOZ0dw0DzK3KnHBZZcF
++0JhFZeLknTYZ5lrPGbH8NQucWDLYngbRQtOoZaxt/+0K3baOqDJf68WP+wk+f6X
+cgmqnI1jnIEboZx6N4/29GhCtqkaoeRzH7ft/a+PCamPnnjwkjnYoQLXJl4XCnwe
+MYun7MZqOmeiSsp3hckeeJb35s8urtins82frkxpi9lSO/l6cp3jl8Urq8ndiz9/
+Ho8FcooGz1+VEiB1c4pK+1X5sYgbQj+n8kRn/A/FGDEc8MrmNtwjooFWbdST4IcU
+LggEC3N1KHofhl/DUQfd1BkwtUEH9KPfhRCGXAA6UBe1kfe+Sz8qlyVYryCerPeL
+wc3R7O9ydCVGJE7wiqU338+YWpl41N6uSVPtO3SNeqKikscvQVdHYuyd145F/EMT
+WuFU5TGWh64ixjzHqA7yHZsaDjVhqlE6NjFlkRxqSD01yHhcCXF6oHghK219uJXF
+4vYNABEBAAGJBFsEGAEIAA8FAmPs+VgCGwIFCQWjmoACQAkQdyH2O9OLR5bBXSAE
+GQEIAAYFAmPs+VgACgkQ6Il5+5swrPJG5Q/+PMhN1qYugsPEQc6trsy3ZLql4evd
+cxulYR1GUDW/OXsBoxg7vw9ubtiRa4QHJpczq8YILy+GvFmrT10Gj6g2WkoeNXpT
+NWGtAu3DUKu8TVQNKXDeW0Pil12TLkGgPPQQpU0lyE8+o+DuKb4QBvMvENhPTL+1
+GGrNDoQ4M1SK8trNaNj5pdao5W/Y3LTvXK0VIher/UbvWkJIBh2LeLsj9x8yg36D
+bs1/1l9ztBZvDTaZyZOqmbCysIO7pFHSTiBCGyyzS1PWWJsrN8DbQyjH5uE+/Wm0
+jcDSJ+HXeYWqR/QQLgyZ5OFpxTmqfQEGT4CV9llygtg10GXkl9VV6SN66+xUm0nn
+PHeW4rcO7NtF1skAdvmaHrUcTYEddOBiIfy2o7WrSyhXPTZz/UpoXsvJ68VWRceh
+7l7Jxjj5G47IhWDLMbT1WJzu9pwQ0wz+GXoyzmmstirQm/KSZAh/FNILqrgxlXfk
+tNl8feO3r8rx6hreVdMlRTw+7gLuwOUAWF77XLc6vd0tY2QyKDD/dznvFaVK1wQX
+4s8x1cT+lVJsTPeyBPoI1UajfT7jK6dg/chAVBpOOH0Fuc8rrqJmGnOzKcdn51oB
+gPwJfboNrr0uKCM1MixCcaXOjPEWJbmnEiIxYAooLnEbL0wcupaGxtRTL50Ms3uv
+nwHim26yvOTrgNQWIQTrTBv9TwQvbd3M7JF3IfY704tHlqW3EACfsMyLwntqn+Qu
+8r3k/6IRn0i9XV/bhStE2y6iHUmqs5sd7dfkmVI7bspoOuDKFIErdTephH09E0hv
+QDJERnMm+rh8TlZtOS/wYywx+2ahSh5Jt3dI5L48ozR+WJbExiXq8ZqTnpn/EQGQ
+8MoM+S2dS+czX85ZL+m3ig+tKHwaaXdvGcYI3h8WwQnX3IBUFCur8WSdfcoGyiQ4
+cpTXcI11GgGgkypxM8wxxoLVCTttpCBRCpPf8/PLKMCK0/k3u4QShtp1WDDQVhFm
+/E6ofG9TSGIKcJmsHHQY7rukEp6lSIvmL0ZjByRah4nK5zoc2j89sNpyuemZwr9X
++V9LOjF7vQTO/8y3cBBNCt0R5lrxeBvRze15k0DzShuHyPhg2PBqfPOS7RnUiF2F
+eI+zQ7xFnLqoD6ckI76RRAf7w0sqnvMlDRpjVU+cDyupR5NdB79oPXJpHltKg4ka
+Q4O5x6BXHVEpAMhJc8bPvmfAiTFac5f0ycibf2R5tNlzbKMD/BxVrzXMghsJ5PWm
+AiUbqPv1II5kLw51b6Bzvl8KzJI0h+ySiUGb86yecfHGbF7zPRch2Kt5+7t0fgEj
+AVcMRfcgHsfQn8EYP9zoczp5Gw7LvR8BBDq1dsTEEEPTDre+HyGxpDN4c8LNGrDa
+CFdXnOdlNV/zT9VvBk/RkV+Tl/Lk4g==
+=Fp5i
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    78178478013521D0
 uid    Ktfmt Team <ktfmt@fb.com>
 
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 373bde6..7fc0903 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -277,6 +277,7 @@
          <trusted-key id="82F833963889D7ED06F1E4DC6525FD70CC303655" group="org.codehaus.mojo"/>
          <trusted-key id="835A685C8C6F49C54980E5CAF406F31BC1468EBA" group="org.jcodec"/>
          <trusted-key id="842AFB86375D805422835BFD82B5574242C20D6F" group="org.antlr"/>
+         <trusted-key id="8461EFA0E74ABAE010DE66994EB27DB2A3B88B8B" group="^androidx[.]test($|([.].*))" regex="true"/>
          <trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1">
             <trusting group="org.apache"/>
             <trusting group="org.apache.maven" name="maven-parent"/>
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index d66e184..1b03d13 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
 # Disable docs
 androidx.enableDocumentation=false
 androidx.playground.snapshotBuildId=11236659
-androidx.playground.metalavaBuildId=11274726
+androidx.playground.metalavaBuildId=11291326
 androidx.studio.type=playground
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/SwipeActivity.kt b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/SwipeActivity.kt
index d7cdecf..3c6d343 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/SwipeActivity.kt
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/SwipeActivity.kt
@@ -18,10 +18,16 @@
 
 import android.app.Activity
 import android.os.Bundle
+import androidx.wear.widget.SwipeDismissFrameLayout
 
 class SwipeActivity : Activity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_swipe)
+        // SwipeDismissFrameLayout is not swipeable by default. It instead relies on system
+        // property `windowSwipeToDismiss` to determine that which is not enabled on mobile devices.
+        // Setting the property explicitly to true to enable the swipe dismiss behaviour.
+        val s2d: SwipeDismissFrameLayout = findViewById(R.id.swipe_dismiss)
+        s2d.isSwipeable = true
     }
 }
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt b/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
index 5a47931..723969a 100644
--- a/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
@@ -21,13 +21,9 @@
 import androidx.benchmark.macro.FrameTimingMetric
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Direction
-import androidx.test.uiautomator.UiDevice
 import androidx.testutils.createCompilationParams
-import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,21 +31,12 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-@Ignore("b/315170517")
 class SwipeBenchmark(
     private val compilationMode: CompilationMode
 ) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
-    private lateinit var device: UiDevice
-
-    @Before
-    fun setUp() {
-        val instrumentation = InstrumentationRegistry.getInstrumentation()
-        device = UiDevice.getInstance(instrumentation)
-    }
-
     @Test
     fun start() {
         benchmarkRule.measureRepeated(
@@ -66,7 +53,7 @@
             val swipeToDismissBox = device.findObject(By.res(PACKAGE_NAME, RESOURCE_ID))
             swipeToDismissBox.setGestureMargin(device.displayWidth / 5)
             repeat(10) {
-                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f)
+                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f, SWIPE_SPEED)
                 device.waitForIdle()
             }
         }
@@ -82,4 +69,5 @@
         @JvmStatic
         fun parameters() = createCompilationParams()
     }
+    private val SWIPE_SPEED = 500
 }
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 2a95d95..460b7f6 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -136,7 +136,7 @@
     method @androidx.compose.runtime.Composable public static void OutlinedCompactChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? label, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ChipColors colors, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material.ChipBorder border);
   }
 
-  @androidx.compose.runtime.Stable public final class Colors {
+  @androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class Colors {
     ctor public Colors(optional long primary, optional long primaryVariant, optional long secondary, optional long secondaryVariant, optional long background, optional long surface, optional long error, optional long onPrimary, optional long onSecondary, optional long onBackground, optional long onSurface, optional long onSurfaceVariant, optional long onError);
     method public androidx.wear.compose.material.Colors copy(optional long primary, optional long primaryVariant, optional long secondary, optional long secondaryVariant, optional long background, optional long surface, optional long error, optional long onPrimary, optional long onSecondary, optional long onBackground, optional long onSurface, optional long onSurfaceVariant, optional long onError);
     method public long getBackground();
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 2a95d95..460b7f6 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -136,7 +136,7 @@
     method @androidx.compose.runtime.Composable public static void OutlinedCompactChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? label, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ChipColors colors, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material.ChipBorder border);
   }
 
-  @androidx.compose.runtime.Stable public final class Colors {
+  @androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class Colors {
     ctor public Colors(optional long primary, optional long primaryVariant, optional long secondary, optional long secondaryVariant, optional long background, optional long surface, optional long error, optional long onPrimary, optional long onSecondary, optional long onBackground, optional long onSurface, optional long onSurfaceVariant, optional long onError);
     method public androidx.wear.compose.material.Colors copy(optional long primary, optional long primaryVariant, optional long secondary, optional long secondaryVariant, optional long background, optional long surface, optional long error, optional long onPrimary, optional long onSecondary, optional long onBackground, optional long onSurface, optional long onSurfaceVariant, optional long onError);
     method public long getBackground();
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ColorsBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ColorsBenchmark.kt
new file mode 100644
index 0000000..63e8507
--- /dev/null
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ColorsBenchmark.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.wear.compose.material.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.contentColorFor
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ColorsBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val colorsTestCaseFactory = { ColorsTestCase() }
+
+    @Test
+    fun firstPixel() {
+        benchmarkRule.benchmarkToFirstPixel(colorsTestCaseFactory)
+    }
+}
+
+private class ColorsTestCase : LayeredComposeTestCase() {
+
+    @Composable
+    override fun MeasuredContent() {
+        MaterialTheme {
+            Column {
+                // Primary
+                Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colors.primary))
+                Box(
+                    modifier = Modifier.size(1.dp)
+                        .background(
+                            MaterialTheme.colors.contentColorFor(MaterialTheme.colors.primary)
+                    )
+                )
+
+                // Secondary
+                Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colors.secondary))
+                Box(
+                    modifier = Modifier.size(1.dp)
+                        .background(
+                            MaterialTheme.colors.contentColorFor(MaterialTheme.colors.secondary)
+                        )
+                )
+
+                // Background
+                Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colors.background))
+                Box(
+                    modifier = Modifier.size(1.dp)
+                        .background(
+                            MaterialTheme.colors.contentColorFor(MaterialTheme.colors.background)
+                        )
+                )
+
+                // Surface
+                Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colors.surface))
+                Box(
+                    modifier = Modifier.size(1.dp)
+                        .background(
+                            MaterialTheme.colors.contentColorFor(MaterialTheme.colors.surface)
+                        )
+                )
+
+                // Error
+                Box(modifier = Modifier.size(1.dp).background(MaterialTheme.colors.error))
+                Box(
+                    modifier = Modifier.size(1.dp)
+                        .background(
+                            MaterialTheme.colors.contentColorFor(MaterialTheme.colors.error)
+                        )
+                )
+            }
+        }
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        content()
+    }
+}
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Colors.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Colors.kt
index 5e0441a..7db6c7c 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Colors.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Colors.kt
@@ -16,64 +16,30 @@
 package androidx.wear.compose.material
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.takeOrElse
 
+@Immutable
 @Stable
 public class Colors(
-    primary: Color = Color(0xFFAECBFA),
-    primaryVariant: Color = Color(0xFF8AB4F8),
-    secondary: Color = Color(0xFFFDE293),
-    secondaryVariant: Color = Color(0xFF594F33),
-    background: Color = Color.Black,
-    surface: Color = Color(0xFF303133),
-    error: Color = Color(0xFFEE675C),
-    onPrimary: Color = Color(0xFF303133),
-    onSecondary: Color = Color(0xFF303133),
-    onBackground: Color = Color.White,
-    onSurface: Color = Color.White,
-    onSurfaceVariant: Color = Color(0xFFDADCE0),
-    onError: Color = Color(0xFF000000)
+    val primary: Color = Color(0xFFAECBFA),
+    val primaryVariant: Color = Color(0xFF8AB4F8),
+    val secondary: Color = Color(0xFFFDE293),
+    val secondaryVariant: Color = Color(0xFF594F33),
+    val background: Color = Color.Black,
+    val surface: Color = Color(0xFF303133),
+    val error: Color = Color(0xFFEE675C),
+    val onPrimary: Color = Color(0xFF303133),
+    val onSecondary: Color = Color(0xFF303133),
+    val onBackground: Color = Color.White,
+    val onSurface: Color = Color.White,
+    val onSurfaceVariant: Color = Color(0xFFDADCE0),
+    val onError: Color = Color(0xFF000000)
 ) {
-    public var primary: Color by mutableStateOf(primary, structuralEqualityPolicy())
-        internal set
-    public var primaryVariant: Color by mutableStateOf(primaryVariant, structuralEqualityPolicy())
-        internal set
-    public var secondary: Color by mutableStateOf(secondary, structuralEqualityPolicy())
-        internal set
-    public var secondaryVariant: Color by mutableStateOf(
-        secondaryVariant,
-        structuralEqualityPolicy()
-    )
-        internal set
-    public var background: Color by mutableStateOf(background, structuralEqualityPolicy())
-        internal set
-    public var surface: Color by mutableStateOf(surface, structuralEqualityPolicy())
-        internal set
-    public var error: Color by mutableStateOf(error, structuralEqualityPolicy())
-        internal set
-    public var onPrimary: Color by mutableStateOf(onPrimary, structuralEqualityPolicy())
-        internal set
-    public var onSecondary: Color by mutableStateOf(onSecondary, structuralEqualityPolicy())
-        internal set
-    public var onBackground: Color by mutableStateOf(onBackground, structuralEqualityPolicy())
-        internal set
-    public var onSurface: Color by mutableStateOf(onSurface, structuralEqualityPolicy())
-        internal set
-    public var onSurfaceVariant: Color by mutableStateOf(
-        onSurfaceVariant,
-        structuralEqualityPolicy()
-    )
-        internal set
-    public var onError: Color by mutableStateOf(onError, structuralEqualityPolicy())
-        internal set
 
     /**
      * Returns a copy of this Colors, optionally overriding some of the values.
@@ -180,35 +146,6 @@
     MaterialTheme.colors.contentColorFor(backgroundColor).takeOrElse { LocalContentColor.current }
 
 /**
- * Updates the internal values of the given [Colors] with values from the [other] [Colors]. This
- * allows efficiently updating a subset of [Colors], without recomposing every composable that
- * consumes values from [LocalColors].
- *
- * Because [Colors] is very wide-reaching, and used by many expensive composables in the
- * hierarchy, providing a new value to [LocalColors] causes every composable consuming
- * [LocalColors] to recompose, which is prohibitively expensive in cases such as animating one
- * color in the theme. Instead, [Colors] is internally backed by [mutableStateOf], and this
- * function mutates the internal state of [this] to match values in [other]. This means that any
- * changes will mutate the internal state of [this], and only cause composables that are reading
- * the specific changed value to recompose.
- */
-internal fun Colors.updateColorsFrom(other: Colors) {
-    primary = other.primary
-    primaryVariant = other.primaryVariant
-    secondary = other.secondary
-    secondaryVariant = other.secondaryVariant
-    background = other.background
-    surface = other.surface
-    error = other.error
-    onPrimary = other.onPrimary
-    onSecondary = other.onSecondary
-    onBackground = other.onBackground
-    onSurface = other.onSurface
-    onSurfaceVariant = other.onSurfaceVariant
-    onError = other.onError
-}
-
-/**
  * Convert given color to disabled color.
  * @param disabledContentAlpha Alpha used to represent disabled content colors.
  */
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
index 5017f99..ed866b9 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.runtime.remember
 import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
 import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
 
@@ -62,16 +61,11 @@
     shapes: Shapes = MaterialTheme.shapes,
     content: @Composable () -> Unit
 ) {
-    val rememberedColors = remember {
-        // Explicitly creating a new object here so we don't mutate the initial [colors]
-        // provided, and overwrite the values set in it.
-        colors.copy()
-    }.apply { updateColorsFrom(colors) }
     val rippleIndication = rippleOrFallbackImplementation()
-    val selectionColors = rememberTextSelectionColors(rememberedColors)
+    val selectionColors = rememberTextSelectionColors(colors)
     @Suppress("DEPRECATION_ERROR")
     CompositionLocalProvider(
-        LocalColors provides rememberedColors,
+        LocalColors provides colors,
         LocalShapes provides shapes,
         LocalTypography provides typography,
         LocalContentAlpha provides ContentAlpha.high,
@@ -79,8 +73,8 @@
         // TODO: b/304985887 - remove after one stable release
         androidx.compose.material.ripple.LocalRippleTheme provides CompatRippleTheme,
         LocalTextSelectionColors provides selectionColors,
-        LocalSwipeToDismissBackgroundScrimColor provides rememberedColors.background,
-        LocalSwipeToDismissContentScrimColor provides rememberedColors.background
+        LocalSwipeToDismissBackgroundScrimColor provides colors.background,
+        LocalSwipeToDismissContentScrimColor provides colors.background
     ) {
         ProvideTextStyle(value = typography.body1, content = content)
     }
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 636bc72..496d3e6 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -680,8 +680,8 @@
 
   public final class ToggleButtonDefaults {
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SplitToggleButtonColors splitToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedSplitContainerColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedSplitContainerColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors toggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedIconColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedIconColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SplitToggleButtonColors splitToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedSplitContainerColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedSplitContainerColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledCheckedSecondaryContentColor, optional long disabledCheckedSplitContainerColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor, optional long disabledUncheckedSecondaryContentColor, optional long disabledUncheckedSplitContainerColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors toggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedIconColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedIconColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledCheckedSecondaryContentColor, optional long disabledCheckedIconColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor, optional long disabledUncheckedSecondaryContentColor, optional long disabledUncheckedIconColor);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     field public static final androidx.wear.compose.material3.ToggleButtonDefaults INSTANCE;
   }
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 636bc72..496d3e6 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -680,8 +680,8 @@
 
   public final class ToggleButtonDefaults {
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SplitToggleButtonColors splitToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedSplitContainerColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedSplitContainerColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors toggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedIconColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedIconColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.SplitToggleButtonColors splitToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedSplitContainerColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedSplitContainerColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledCheckedSecondaryContentColor, optional long disabledCheckedSplitContainerColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor, optional long disabledUncheckedSecondaryContentColor, optional long disabledUncheckedSplitContainerColor);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors toggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedIconColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedIconColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledCheckedSecondaryContentColor, optional long disabledCheckedIconColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor, optional long disabledUncheckedSecondaryContentColor, optional long disabledUncheckedIconColor);
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
     field public static final androidx.wear.compose.material3.ToggleButtonDefaults INSTANCE;
   }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ToggleButtonTest.kt
index 748403a..95f5df1 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ToggleButtonTest.kt
@@ -18,14 +18,18 @@
 
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.testutils.assertContainsColor
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -41,6 +45,7 @@
 import androidx.compose.ui.test.assertIsOn
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onChildAt
 import androidx.compose.ui.test.onNodeWithTag
@@ -52,6 +57,7 @@
 import androidx.wear.compose.material3.samples.SplitToggleButtonWithSwitch
 import androidx.wear.compose.material3.samples.ToggleButtonWithCheckbox
 import androidx.wear.compose.material3.samples.ToggleButtonWithSwitch
+import org.junit.Assert
 import org.junit.Rule
 import org.junit.Test
 
@@ -559,6 +565,54 @@
         )
 
     @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_toggle_button_colors_enabled_and_checked() {
+        rule.verifyToggleButtonColors(checked = true, enabled = true)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_toggle_button_colors_enabled_and_unchecked() {
+        rule.verifyToggleButtonColors(checked = false, enabled = true)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_toggle_button_colors_disabled_and_checked() {
+        rule.verifyToggleButtonColors(checked = true, enabled = false)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_toggle_button_colors_disabled_and_unchecked() {
+        rule.verifyToggleButtonColors(checked = false, enabled = false)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_split_toggle_button_colors_enabled_and_checked() {
+        rule.verifySplitToggleButtonColors(checked = true, enabled = true)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_split_toggle_button_colors_enabled_and_unchecked() {
+        rule.verifySplitToggleButtonColors(checked = false, enabled = true)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_split_toggle_button_colors_disabled_and_checked() {
+        rule.verifySplitToggleButtonColors(checked = true, enabled = false)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun verify_split_toggle_button_colors_disabled_and_unchecked() {
+        rule.verifySplitToggleButtonColors(checked = false, enabled = false)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
     private fun verifyToggleButtonBackgroundColor(
         checked: Boolean,
         enabled: Boolean,
@@ -658,5 +712,143 @@
     toggleControl = toggleControl,
 )
 
+@RequiresApi(Build.VERSION_CODES.O)
+private fun ComposeContentTestRule.verifyToggleButtonColors(
+    enabled: Boolean,
+    checked: Boolean
+) {
+    val testBackgroundColor = Color.White
+    var expectedContainerColor = Color.Transparent
+    var expectedLabelColor = Color.Transparent
+    var expectedIconColor = Color.Transparent
+    var expectedSecondaryLabelColor = Color.Transparent
+    var actualLabelColor = Color.Transparent
+    var actualIconColor = Color.Transparent
+    var actualSecondaryLabelColor = Color.Transparent
+    setContentWithTheme {
+        expectedContainerColor = toggle_button_container_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+            .compositeOver(testBackgroundColor)
+        expectedLabelColor = toggle_button_content_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+        expectedSecondaryLabelColor = toggle_button_secondary_label_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+        expectedIconColor = toggle_button_icon_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+        Box(
+            Modifier
+                .fillMaxSize()
+                .background(testBackgroundColor)
+        ) {
+            ToggleButton(
+                modifier = Modifier.testTag(TEST_TAG),
+                checked = checked,
+                onCheckedChange = {},
+                enabled = enabled,
+                toggleControl = { Checkbox() },
+                label = { actualLabelColor = LocalContentColor.current },
+                secondaryLabel = { actualSecondaryLabelColor = LocalContentColor.current },
+                icon = { actualIconColor = LocalContentColor.current }
+            )
+        }
+    }
+    Assert.assertEquals(expectedLabelColor, actualLabelColor)
+    Assert.assertEquals(expectedSecondaryLabelColor, actualSecondaryLabelColor)
+    Assert.assertEquals(expectedIconColor, actualIconColor)
+
+    onNodeWithTag(TEST_TAG)
+        .captureToImage()
+        .assertContainsColor(
+            if (expectedContainerColor != Color.Transparent) expectedContainerColor
+            else testBackgroundColor,
+        )
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+private fun ComposeContentTestRule.verifySplitToggleButtonColors(
+    enabled: Boolean,
+    checked: Boolean
+) {
+    val testBackgroundColor = Color.White
+    var expectedContainerColor = Color.Transparent
+    var expectedLabelColor = Color.Transparent
+    var expectedSecondaryLabelColor = Color.Transparent
+    var actualLabelColor = Color.Transparent
+    var actualSecondaryLabelColor = Color.Transparent
+    setContentWithTheme {
+        expectedContainerColor = toggle_button_container_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+            .compositeOver(testBackgroundColor)
+        expectedLabelColor = toggle_button_content_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+        expectedSecondaryLabelColor = toggle_button_secondary_label_color(checked)
+            .withDisabledAlphaApplied(enabled = enabled)
+        Box(
+            Modifier
+                .fillMaxSize()
+                .background(testBackgroundColor)
+        ) {
+            SplitToggleButton(
+                modifier = Modifier.testTag(TEST_TAG),
+                checked = checked,
+                onCheckedChange = {},
+                onClick = {},
+                enabled = enabled,
+                toggleControl = { Checkbox() },
+                label = { actualLabelColor = LocalContentColor.current },
+                secondaryLabel = { actualSecondaryLabelColor = LocalContentColor.current },
+            )
+        }
+    }
+    Assert.assertEquals(expectedLabelColor, actualLabelColor)
+    Assert.assertEquals(expectedSecondaryLabelColor, actualSecondaryLabelColor)
+
+    onNodeWithTag(TEST_TAG)
+        .captureToImage()
+        .assertContainsColor(
+            if (expectedContainerColor != Color.Transparent) expectedContainerColor
+            else testBackgroundColor,
+        )
+}
+
+@Composable
+private fun toggle_button_container_color(
+    checked: Boolean
+): Color {
+    return if (checked) MaterialTheme.colorScheme.primaryContainer
+    else MaterialTheme.colorScheme.surface
+}
+
+@Composable
+private fun toggle_button_content_color(
+    checked: Boolean
+): Color {
+    return if (checked) MaterialTheme.colorScheme.onPrimaryContainer
+    else MaterialTheme.colorScheme.onSurface
+}
+
+@Composable
+private fun toggle_button_secondary_label_color(
+    checked: Boolean
+): Color {
+    return if (checked) MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
+    else MaterialTheme.colorScheme.onSurfaceVariant
+}
+
+@Composable
+private fun toggle_button_icon_color(
+    checked: Boolean
+): Color {
+    return if (checked) MaterialTheme.colorScheme.onPrimaryContainer
+    else MaterialTheme.colorScheme.primary
+}
+
+@Composable
+private fun Color.withDisabledAlphaApplied(
+    enabled: Boolean
+): Color {
+    return if (!enabled) toDisabledColor(disabledAlpha = 0.38f) else this
+}
+
 private val CHECKED_COLOR = Color(0xFFA020F0)
 private val UNCHECKED_COLOR = Color(0xFFFFA500)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
index 2c207e0..04506ec 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
@@ -36,6 +36,8 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.material3.tokens.MotionTokens
+import androidx.wear.compose.material3.tokens.SplitToggleButtonTokens
+import androidx.wear.compose.material3.tokens.ToggleButtonTokens
 import androidx.wear.compose.materialcore.animateSelectionColor
 
 /**
@@ -81,8 +83,7 @@
  * interactions will still happen internally.
  * @param icon An optional slot for providing an icon to indicate the purpose of the button. The
  * contents are expected to be a horizontally and vertically center aligned icon of size
- * 24.dp. In order to correctly render when the Chip is not enabled the
- * icon must set its alpha value to [LocalContentAlpha].
+ * 24.dp.
  * @param secondaryLabel A slot for providing the button's secondary label. The contents are
  * expected to be text which is "start" aligned.
  * @param label A slot for providing the button's main label. The contents are expected to be text
@@ -95,7 +96,7 @@
     toggleControl: @Composable ToggleControlScope.() -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = MaterialTheme.shapes.large,
+    shape: Shape = ToggleButtonTokens.ContainerShape.value,
     colors: ToggleButtonColors = ToggleButtonDefaults.toggleButtonColors(),
     contentPadding: PaddingValues = ToggleButtonDefaults.ContentPadding,
     interactionSource: MutableInteractionSource? = null,
@@ -108,7 +109,7 @@
         onCheckedChange = onCheckedChange,
         label = provideScopeContent(
             contentColor = colors.contentColor(enabled = enabled, checked),
-            textStyle = MaterialTheme.typography.labelMedium,
+            textStyle = ToggleButtonTokens.LabelFont.value,
             content = label
         ),
         toggleControl = {
@@ -124,7 +125,7 @@
         ),
         secondaryLabel = provideNullableScopeContent(
             contentColor = colors.secondaryContentColor(enabled = enabled, checked),
-            textStyle = MaterialTheme.typography.labelSmall,
+            textStyle = ToggleButtonTokens.SecondaryLabelFont.value,
             content = secondaryLabel
         ),
         background = { isEnabled, isChecked ->
@@ -210,7 +211,7 @@
     toggleControl: @Composable ToggleControlScope.() -> Unit,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
-    shape: Shape = MaterialTheme.shapes.large,
+    shape: Shape = SplitToggleButtonTokens.ContainerShape.value,
     colors: SplitToggleButtonColors = ToggleButtonDefaults.splitToggleButtonColors(),
     checkedInteractionSource: MutableInteractionSource? = null,
     clickInteractionSource: MutableInteractionSource? = null,
@@ -222,7 +223,7 @@
     onCheckedChange = onCheckedChange,
     label = provideScopeContent(
         contentColor = colors.contentColor(enabled = enabled, checked = checked),
-        textStyle = MaterialTheme.typography.labelMedium,
+        textStyle = SplitToggleButtonTokens.LabelFont.value,
         content = label
     ),
     onClick = onClick,
@@ -235,7 +236,7 @@
         .height(IntrinsicSize.Min),
     secondaryLabel = provideNullableScopeContent(
         contentColor = colors.secondaryContentColor(enabled = enabled, checked = checked),
-        textStyle = MaterialTheme.typography.labelSmall,
+        textStyle = SplitToggleButtonTokens.SecondaryLabelFont.value,
         content = secondaryLabel
     ),
     backgroundColor = { isEnabled, isChecked ->
@@ -282,19 +283,60 @@
      * when enabled and unchecked, used for secondaryLabel content
      * @param uncheckedIconColor The icon color of the [ToggleButton]
      * when enabled and unchecked.
+     * @param disabledCheckedContainerColor The container color of the [ToggleButton]
+     * when disabled and checked.
+     * @param disabledCheckedContentColor The content color of the [ToggleButton]
+     * when disabled and checked.
+     * @param disabledCheckedSecondaryContentColor The secondary content color of the
+     * [ToggleButton] when disabled and checked, used for secondaryLabel content.
+     * @param disabledCheckedIconColor The icon color of the [ToggleButton]
+     * when disabled and checked.
+     * @param disabledUncheckedContainerColor  The container color of the [ToggleButton]
+     * when disabled and unchecked.
+     * @param disabledUncheckedContentColor The content color of a [ToggleButton]
+     * when disabled and unchecked.
+     * @param disabledUncheckedSecondaryContentColor The secondary content color of this
+     * [ToggleButton] when disabled and unchecked, used for secondaryLabel content
+     * @param disabledUncheckedIconColor The icon color of the [ToggleButton]
+     * when disabled and unchecked.
      */
     @Composable
     fun toggleButtonColors(
-        checkedContainerColor: Color = MaterialTheme.colorScheme.primaryContainer,
-        checkedContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
-        checkedSecondaryContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer.copy(
-            alpha = 0.8f
-        ),
-        checkedIconColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
-        uncheckedContainerColor: Color = MaterialTheme.colorScheme.surface,
-        uncheckedContentColor: Color = MaterialTheme.colorScheme.onSurface,
-        uncheckedSecondaryContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
-        uncheckedIconColor: Color = MaterialTheme.colorScheme.primary,
+        checkedContainerColor: Color = ToggleButtonTokens.CheckedContainerColor.value,
+        checkedContentColor: Color = ToggleButtonTokens.CheckedContentColor.value,
+        checkedSecondaryContentColor: Color = ToggleButtonTokens.CheckedSecondaryLabelColor.value
+            .copy(alpha = ToggleButtonTokens.CheckedSecondaryLabelOpacity),
+        checkedIconColor: Color = ToggleButtonTokens.CheckedIconColor.value,
+        uncheckedContainerColor: Color = ToggleButtonTokens.UncheckedContainerColor.value,
+        uncheckedContentColor: Color = ToggleButtonTokens.UncheckedContentColor.value,
+        uncheckedSecondaryContentColor: Color =
+            ToggleButtonTokens.UncheckedSecondaryLabelColor.value,
+        uncheckedIconColor: Color = ToggleButtonTokens.UncheckedIconColor.value,
+        disabledCheckedContainerColor: Color =
+            ToggleButtonTokens.DisabledCheckedContainerColor.value.toDisabledColor(
+                disabledAlpha = ToggleButtonTokens.DisabledOpacity
+            ),
+        disabledCheckedContentColor: Color = ToggleButtonTokens.DisabledCheckedContentColor.value
+            .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
+        disabledCheckedSecondaryContentColor: Color =
+            ToggleButtonTokens.DisabledCheckedSecondaryLabelColor.value
+                .copy(alpha = ToggleButtonTokens.DisabledCheckedSecondaryLabelOpacity)
+                .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
+        disabledCheckedIconColor: Color = ToggleButtonTokens.DisabledCheckedIconColor.value
+            .toDisabledColor(
+                disabledAlpha = ToggleButtonTokens.DisabledOpacity
+            ),
+        disabledUncheckedContainerColor: Color =
+            ToggleButtonTokens.DisabledUncheckedContainerColor.value
+                .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedContentColor: Color =
+            ToggleButtonTokens.DisabledUncheckedContentColor.value
+                .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedSecondaryContentColor: Color =
+            ToggleButtonTokens.DisabledUncheckedSecondaryLabelColor.value
+                .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedIconColor: Color = ToggleButtonTokens.DisabledUncheckedIconColor.value
+            .toDisabledColor(disabledAlpha = ToggleButtonTokens.DisabledOpacity),
     ) =
         ToggleButtonColors(
             checkedContainerColor = checkedContainerColor,
@@ -305,15 +347,14 @@
             uncheckedContentColor = uncheckedContentColor,
             uncheckedSecondaryContentColor = uncheckedSecondaryContentColor,
             uncheckedIconColor = uncheckedIconColor,
-            disabledCheckedContainerColor = checkedContainerColor.toDisabledColor(),
-            disabledCheckedContentColor = checkedContentColor.toDisabledColor(),
-            disabledCheckedSecondaryContentColor = checkedSecondaryContentColor.toDisabledColor(),
-            disabledCheckedIconColor = checkedIconColor.toDisabledColor(),
-            disabledUncheckedContainerColor = uncheckedContainerColor.toDisabledColor(),
-            disabledUncheckedContentColor = uncheckedContentColor.toDisabledColor(),
-            disabledUncheckedSecondaryContentColor =
-            uncheckedSecondaryContentColor.toDisabledColor(),
-            disabledUncheckedIconColor = uncheckedIconColor.toDisabledColor(),
+            disabledCheckedContainerColor = disabledCheckedContainerColor,
+            disabledCheckedContentColor = disabledCheckedContentColor,
+            disabledCheckedSecondaryContentColor = disabledCheckedSecondaryContentColor,
+            disabledCheckedIconColor = disabledCheckedIconColor,
+            disabledUncheckedContainerColor = disabledUncheckedContainerColor,
+            disabledUncheckedContentColor = disabledUncheckedContentColor,
+            disabledUncheckedSecondaryContentColor = disabledUncheckedSecondaryContentColor,
+            disabledUncheckedIconColor = disabledUncheckedIconColor,
         )
 
     /**
@@ -335,19 +376,65 @@
      * when enabled and unchecked, used for secondaryLabel content.
      * @param uncheckedSplitContainerColor The split container color of the [SplitToggleButton] when
      * enabled and unchecked.
+     * @param disabledCheckedContainerColor The container color of the [SplitToggleButton] when
+     * disabled and checked.
+     * @param disabledCheckedContentColor The content color of the [SplitToggleButton] when
+     * disabled and checked.
+     * @param disabledCheckedSecondaryContentColor The secondary content color of the
+     * [SplitToggleButton] when disabled and checked, used for secondaryLabel content.
+     * @param disabledCheckedSplitContainerColor The split container color of the [
+     * SplitToggleButton] when disabled and checked.
+     * @param disabledUncheckedContainerColor The container color of the [SplitToggleButton] when
+     * disabled and unchecked.
+     * @param disabledUncheckedContentColor The content color of the [SplitToggleButton] when
+     * disabled and unchecked.
+     * @param disabledUncheckedSecondaryContentColor The secondary content color of the
+     * [SplitToggleButton] when disabled and unchecked, used for secondaryLabel content.
+     * @param disabledUncheckedSplitContainerColor The split container color of the
+     * [SplitToggleButton] when disabled and unchecked.
      */
     @Composable
     fun splitToggleButtonColors(
-        checkedContainerColor: Color = MaterialTheme.colorScheme.primaryContainer,
-        checkedContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
-        checkedSecondaryContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer.copy(
-            alpha = 0.8f
-        ),
-        checkedSplitContainerColor: Color = MaterialTheme.colorScheme.primary.copy(.15f),
-        uncheckedContainerColor: Color = MaterialTheme.colorScheme.surface,
-        uncheckedContentColor: Color = MaterialTheme.colorScheme.onSurface,
-        uncheckedSecondaryContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
-        uncheckedSplitContainerColor: Color = MaterialTheme.colorScheme.surfaceBright
+        checkedContainerColor: Color = SplitToggleButtonTokens.CheckedContainerColor.value,
+        checkedContentColor: Color = SplitToggleButtonTokens.CheckedContentColor.value,
+        checkedSecondaryContentColor: Color = SplitToggleButtonTokens.CheckedSecondaryLabelColor
+            .value
+            .copy(alpha = SplitToggleButtonTokens.CheckedSecondaryLabelOpacity),
+        checkedSplitContainerColor: Color = SplitToggleButtonTokens.CheckedSplitContainerColor
+            .value
+            .copy(alpha = SplitToggleButtonTokens.CheckedSplitContainerOpacity),
+        uncheckedContainerColor: Color = SplitToggleButtonTokens.UncheckedContainerColor.value,
+        uncheckedContentColor: Color = SplitToggleButtonTokens.UncheckedContentColor.value,
+        uncheckedSecondaryContentColor: Color =
+            SplitToggleButtonTokens.UncheckedSecondaryLabelColor.value,
+        uncheckedSplitContainerColor: Color =
+            SplitToggleButtonTokens.UncheckedSplitContainerColor.value,
+        disabledCheckedContainerColor: Color =
+            SplitToggleButtonTokens.DisabledCheckedContainerColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledCheckedContentColor: Color =
+            SplitToggleButtonTokens.DisabledCheckedContentColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledCheckedSecondaryContentColor: Color =
+            SplitToggleButtonTokens.DisabledCheckedSecondaryLabelColor.value
+                .copy(alpha = SplitToggleButtonTokens.DisabledCheckedSecondaryLabelOpacity)
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledCheckedSplitContainerColor: Color =
+            SplitToggleButtonTokens.DisabledCheckedSplitContainerColor.value
+                .copy(alpha = SplitToggleButtonTokens.DisabledCheckedSplitContainerOpacity)
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedContainerColor: Color =
+            SplitToggleButtonTokens.DisabledUncheckedContainerColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedContentColor: Color =
+            SplitToggleButtonTokens.DisabledUncheckedContentColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedSecondaryContentColor: Color =
+            SplitToggleButtonTokens.DisabledUncheckedSecondaryLabelColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity),
+        disabledUncheckedSplitContainerColor: Color =
+            SplitToggleButtonTokens.DisabledUncheckedSplitContainerColor.value
+                .toDisabledColor(disabledAlpha = SplitToggleButtonTokens.DisabledOpacity)
     ) =
         SplitToggleButtonColors(
             checkedContainerColor = checkedContainerColor,
@@ -358,15 +445,14 @@
             uncheckedContentColor = uncheckedContentColor,
             uncheckedSecondaryContentColor = uncheckedSecondaryContentColor,
             uncheckedSplitContainerColor = uncheckedSplitContainerColor,
-            disabledCheckedContainerColor = checkedContainerColor.toDisabledColor(),
-            disabledCheckedContentColor = checkedContentColor.toDisabledColor(),
-            disabledCheckedSecondaryContentColor = checkedSecondaryContentColor.toDisabledColor(),
-            disabledCheckedSplitContainerColor = checkedSplitContainerColor.toDisabledColor(),
-            disabledUncheckedContainerColor = uncheckedContainerColor.toDisabledColor(),
-            disabledUncheckedContentColor = uncheckedContentColor.toDisabledColor(),
-            disabledUncheckedSecondaryContentColor =
-            uncheckedSecondaryContentColor.toDisabledColor(),
-            disabledUncheckedSplitContainerColor = uncheckedSplitContainerColor.toDisabledColor()
+            disabledCheckedContainerColor = disabledCheckedContainerColor,
+            disabledCheckedContentColor = disabledCheckedContentColor,
+            disabledCheckedSecondaryContentColor = disabledCheckedSecondaryContentColor,
+            disabledCheckedSplitContainerColor = disabledCheckedSplitContainerColor,
+            disabledUncheckedContainerColor = disabledUncheckedContainerColor,
+            disabledUncheckedContentColor = disabledUncheckedContentColor,
+            disabledUncheckedSecondaryContentColor = disabledUncheckedSecondaryContentColor,
+            disabledUncheckedSplitContainerColor = disabledUncheckedSplitContainerColor
         )
 
     private val ChipHorizontalPadding = 14.dp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SplitToggleButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SplitToggleButtonTokens.kt
new file mode 100644
index 0000000..6241bb6
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SplitToggleButtonTokens.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_40
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object SplitToggleButtonTokens {
+    val CheckedContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val CheckedContentColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val CheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val CheckedSecondaryLabelOpacity = 0.8f
+    val CheckedSplitContainerColor = ColorSchemeKeyTokens.Primary
+    val CheckedSplitContainerOpacity = 0.15f
+    val ContainerShape = ShapeKeyTokens.CornerLarge
+    val DisabledCheckedContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val DisabledCheckedContentColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val DisabledCheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val DisabledCheckedSecondaryLabelOpacity = 0.8f
+    val DisabledCheckedSplitContainerColor = ColorSchemeKeyTokens.Primary
+    val DisabledCheckedSplitContainerOpacity = 0.15f
+    val DisabledOpacity = 0.38f
+    val DisabledUncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val DisabledUncheckedContentColor = ColorSchemeKeyTokens.OnSurface
+    val DisabledUncheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val DisabledUncheckedSplitContainerColor = ColorSchemeKeyTokens.SurfaceBright
+    val LabelFont = TypographyKeyTokens.LabelMedium
+    val SecondaryLabelFont = TypographyKeyTokens.LabelSmall
+    val UncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val UncheckedContentColor = ColorSchemeKeyTokens.OnSurface
+    val UncheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val UncheckedSplitContainerColor = ColorSchemeKeyTokens.SurfaceBright
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ToggleButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ToggleButtonTokens.kt
new file mode 100644
index 0000000..c211e57
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ToggleButtonTokens.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_40
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.wear.compose.material3.tokens
+
+internal object ToggleButtonTokens {
+    val CheckedContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val CheckedContentColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val CheckedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val CheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val CheckedSecondaryLabelOpacity = 0.8f
+    val ContainerShape = ShapeKeyTokens.CornerLarge
+    val DisabledCheckedContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val DisabledCheckedContentColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val DisabledCheckedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val DisabledCheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val DisabledCheckedSecondaryLabelOpacity = 0.8f
+    val DisabledOpacity = 0.38f
+    val DisabledUncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val DisabledUncheckedContentColor = ColorSchemeKeyTokens.OnSurface
+    val DisabledUncheckedIconColor = ColorSchemeKeyTokens.Primary
+    val DisabledUncheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val LabelFont = TypographyKeyTokens.LabelMedium
+    val SecondaryLabelFont = TypographyKeyTokens.LabelSmall
+    val UncheckedContainerColor = ColorSchemeKeyTokens.Surface
+    val UncheckedContentColor = ColorSchemeKeyTokens.OnSurface
+    val UncheckedIconColor = ColorSchemeKeyTokens.Primary
+    val UncheckedSecondaryLabelColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
diff --git a/wear/protolayout/protolayout-material-core/build.gradle b/wear/protolayout/protolayout-material-core/build.gradle
index dcf081a..fdab6c1 100644
--- a/wear/protolayout/protolayout-material-core/build.gradle
+++ b/wear/protolayout/protolayout-material-core/build.gradle
@@ -44,7 +44,7 @@
     annotationProcessor(libs.nullaway)
     api("androidx.annotation:annotation:1.2.0")
     api(project(":wear:protolayout:protolayout"))
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
 
     testImplementation(libs.junit)
     testImplementation(libs.mockitoCore4)
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index a09bf05..11934f2 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -34,7 +34,7 @@
     api("androidx.annotation:annotation:1.2.0")
     api(project(":wear:protolayout:protolayout"))
     implementation(project(":wear:protolayout:protolayout-material-core"))
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation(project(":annotation:annotation-experimental"))
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testCore)
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/layouts/MultiButtonLayout.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/layouts/MultiButtonLayout.java
index d7ed740..3f9f9ff 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/layouts/MultiButtonLayout.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/layouts/MultiButtonLayout.java
@@ -136,7 +136,12 @@
             return this;
         }
 
-        /** Constructs and returns {@link MultiButtonLayout} with the provided content and look. */
+        /**
+         * Constructs and returns {@link MultiButtonLayout} with the provided content and look.
+         *
+         * @throws IllegalArgumentException if no buttons are added or the number of buttons added
+         *      is larger than {@link LayoutDefaults.MultiButtonLayoutDefaults#MAX_BUTTONS}.
+         */
         @NonNull
         @Override
         public MultiButtonLayout build() {
@@ -145,6 +150,9 @@
                 throw new IllegalArgumentException(
                         "Too many buttons are added. Maximum number is " + MAX_BUTTONS + ".");
             }
+            if (buttonNum == 0) {
+                throw new IllegalArgumentException("No buttons are added. Minimum number is 1.");
+            }
 
             LayoutElement buttons = buildButtons(buttonNum);
             Box.Builder elementBuilder =
@@ -247,8 +255,9 @@
                                             BUTTON_SIZE_FOR_3_PLUS_BUTTONS))
                             .build();
                 default:
-                    throw new IllegalArgumentException(
-                            "Too many buttons are added. Maximum number is " + MAX_BUTTONS + ".");
+                    // This shouldn't happen as we have min/max checks above.
+                    throw new IllegalStateException(
+                            "Incorrect number of buttons when building MultiButtonLayout.");
             }
         }
 
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index f233a02..4a650a4 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -150,8 +150,11 @@
   TEXT_OVERFLOW_TRUNCATE = 1;
 
   // Truncate the text to fit in the Text element's bounds, but add an ellipsis
-  // (i.e. ...) to the end of the text if it has been truncated.
-  // @deprecated Use TEXT_OVERFLOW_ELLIPSIZE instead.
+  // (i.e. ...) to the end of the text if it has been truncated. Note that this will not add an
+  // ellipsis if the number of lines that fits into the available space is less than the
+  // {@code setMaxLines} in Text.
+  //
+  // DEPRECATED: Use {@link #TEXT_OVERFLOW_ELLIPSIZE} instead.
   TEXT_OVERFLOW_ELLIPSIZE_END = 2;
 
   // Enable marquee animation for texts that don't fit inside the Text element.
@@ -163,13 +166,9 @@
 
   // Truncate the text to fit in the Text element's parent bounds, but add an
   // ellipsis (i.e. ...) to the end of the text if it has been truncated.
-  // This will truncate the text even before {@code setMaxLines} in Text is
-  // reached if there's not enough space in the parent container.
-  // Note that, when this is used, the parent of the Text element this
-  // corresponds to shouldn't have its width and height set to wrapped, as it
-  // can lead to unexpected results.
-  // <p>Note that, on SpanText, this will behave exactly the same way as
-  // TEXT_OVERFLOW_ELLIPSIZE_END.
+  //
+  // <p>Note that, when this is used, the parent of the Text element this corresponds to shouldn't
+  // have its width and height set to wrapped, as it can lead to unexpected results.
   TEXT_OVERFLOW_ELLIPSIZE = 4;
 }
 
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index cc2daa2..7263518 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -18,10 +18,12 @@
 
 import static android.util.TypedValue.COMPLEX_UNIT_SP;
 import static android.view.View.INVISIBLE;
+import static android.view.View.LAYOUT_DIRECTION_LTR;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
 import static android.view.View.VISIBLE;
-import static androidx.wear.protolayout.proto.LayoutElementProto.ArcDirection.ARC_DIRECTION_CLOCKWISE_VALUE;
 
 import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.wear.protolayout.proto.LayoutElementProto.ArcDirection.ARC_DIRECTION_CLOCKWISE_VALUE;
 import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.FIRST_CHILD_INDEX;
 import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.ROOT_NODE_ID;
 import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
@@ -53,6 +55,7 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextPaint;
+import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
 import android.text.method.LinkMovementMethod;
 import android.text.style.AbsoluteSizeSpan;
@@ -114,6 +117,7 @@
 import androidx.wear.protolayout.proto.DimensionProto.DpProp;
 import androidx.wear.protolayout.proto.DimensionProto.ExpandedAngularDimensionProp;
 import androidx.wear.protolayout.proto.DimensionProto.ExpandedDimensionProp;
+import androidx.wear.protolayout.proto.DimensionProto.ExtensionDimension;
 import androidx.wear.protolayout.proto.DimensionProto.ImageDimension;
 import androidx.wear.protolayout.proto.DimensionProto.ProportionalDimensionProp;
 import androidx.wear.protolayout.proto.DimensionProto.SpProp;
@@ -190,6 +194,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CancellationException;
@@ -199,11 +204,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import static android.view.View.LAYOUT_DIRECTION_LTR;
-import static android.view.View.LAYOUT_DIRECTION_RTL;
-import android.text.TextUtils;
-import java.util.Locale;
-
 /**
  * Renderer for ProtoLayout.
  *
@@ -1657,8 +1657,6 @@
                 case SlideDirection.SLIDE_DIRECTION_BOTTOM_TO_TOP_VALUE:
                     fromYDelta = getInitialOffsetOrDefaultY(slideIn, view);
                     break;
-                default:
-                    break;
             }
 
             TranslateAnimation translateAnimation =
@@ -1711,8 +1709,6 @@
                     case SlideDirection.SLIDE_DIRECTION_BOTTOM_TO_TOP_VALUE:
                         toYDelta = getTargetOffsetOrDefaultY(slideOut, view);
                         break;
-                    default:
-                        break;
                 }
 
                 TranslateAnimation translateAnimation =
@@ -3118,7 +3114,7 @@
 
         lineView.setThickness(thicknessPx);
 
-        DegreesProp length;
+        DegreesProp length = DegreesProp.getDefaultInstance();
         if (line.hasAngularLength()) {
             final ArcLineLength angularLength = line.getAngularLength();
             switch (angularLength.getInnerCase()) {
@@ -3139,8 +3135,7 @@
                         break;
                     }
 
-                default:
-                    length = DegreesProp.getDefaultInstance();
+                case INNER_NOT_SET:
                     break;
             }
         } else {
@@ -3205,6 +3200,7 @@
             LayoutInfo.Builder layoutInfoBuilder,
             Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
         ArcLayout arcLayout = new ArcLayout(mUiContext);
+        int anchorAngleSign = 1;
 
         if (arc.hasArcDirection()) {
             switch (arc.getArcDirection().getValue()) {
@@ -3213,12 +3209,17 @@
                     break;
                 case ARC_DIRECTION_COUNTER_CLOCKWISE:
                     arcLayout.setLayoutDirection(LAYOUT_DIRECTION_RTL);
+                    anchorAngleSign = -1;
                     break;
                 case ARC_DIRECTION_NORMAL:
-                case UNRECOGNIZED:
+                    boolean isRtl = isRtlLayoutDirectionFromLocale();
                     arcLayout.setLayoutDirection(
-                            isRtlLayoutDirectionFromLocale() ?
-                                    LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR);
+                            isRtl ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR);
+                    if (isRtl) {
+                        anchorAngleSign = -1;
+                    }
+                    break;
+                case UNRECOGNIZED:
                     break;
             }
         }
@@ -3227,17 +3228,18 @@
         layoutParams.width = LayoutParams.MATCH_PARENT;
         layoutParams.height = LayoutParams.MATCH_PARENT;
 
+        int finalAnchorAngleSign = anchorAngleSign;
         handleProp(
                 arc.getAnchorAngle(),
                 angle -> {
-                    arcLayout.setAnchorAngleDegrees(angle);
+                    arcLayout.setAnchorAngleDegrees(finalAnchorAngleSign * angle);
                     // Invalidating arcLayout isn't enough. AnchorAngleDegrees change should trigger
                     // child requestLayout.
                     arcLayout.requestLayout();
                 },
                 arcPosId,
                 pipelineMaker);
-        arcLayout.setAnchorAngleDegrees(arc.getAnchorAngle().getValue());
+        arcLayout.setAnchorAngleDegrees(finalAnchorAngleSign * arc.getAnchorAngle().getValue());
         arcLayout.setAnchorType(anchorTypeToAnchorPos(arc.getAnchorType().getValue()));
 
         if (arc.hasMaxAngle()) {
@@ -3525,7 +3527,7 @@
                         excludeFontPadding = true;
                     }
                     break;
-                default:
+                case INNER_NOT_SET:
                     Log.w(TAG, "Unknown Span child type.");
                     break;
             }
@@ -4043,11 +4045,12 @@
             case TEXT:
             case SPANNABLE:
                 return true;
+            case EXTENSION:
+                return isMeasurable(element.getExtension().getWidth());
             case INNER_NOT_SET:
                 return false;
-            default: // TODO(b/276703002): Remove default case
-                return false;
         }
+        return false;
     }
 
     private boolean isHeightMeasurable(LayoutElement element, ContainerDimension containerWidth) {
@@ -4078,11 +4081,12 @@
             case TEXT:
             case SPANNABLE:
                 return true;
+            case EXTENSION:
+                return isMeasurable(element.getExtension().getHeight());
             case INNER_NOT_SET:
                 return false;
-            default: // TODO(b/276703002): Remove default case
-                return false;
         }
+        return false;
     }
 
     private boolean isMeasurable(ContainerDimension dimension) {
@@ -4113,6 +4117,16 @@
         return false;
     }
 
+    private static boolean isMeasurable(ExtensionDimension dimension) {
+        switch (dimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+                return true;
+            case INNER_NOT_SET:
+                return false;
+        }
+        return false;
+    }
+
     private void inflateChildElements(
             @NonNull ViewGroup parent,
             @NonNull LayoutParams parentLayoutParams,
@@ -4458,9 +4472,11 @@
                 return "android.widget.Switch";
             case SEMANTICS_ROLE_RADIOBUTTON:
                 return "android.widget.RadioButton";
-            default:
+            case SEMANTICS_ROLE_NONE:
+            case UNRECOGNIZED:
                 return "";
         }
+        return "";
     }
 
     // getObsoleteContentDescription is used for backward compatibility
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 950e73f..c02a19a 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -47,6 +47,7 @@
 import static org.robolectric.Shadows.shadowOf;
 
 import static java.lang.Integer.MAX_VALUE;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.app.Activity;
 import android.app.Application;
@@ -224,7 +225,6 @@
 import org.robolectric.shadows.ShadowSystemClock;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -536,6 +536,54 @@
     }
 
     @Test
+    public void inflate_box_withExtensionElement() {
+        int width = 10;
+        int height = 12;
+        byte[] payload = "Hello World".getBytes(UTF_8);
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                    .setBox(
+                        Box.newBuilder()
+                            // Outer box's width and height left at default value of "wrap"
+                            .addContents(
+                                LayoutElement.newBuilder()
+                                    .setExtension(
+                                        ExtensionLayoutElement.newBuilder()
+                                            .setExtensionId("foo")
+                                            .setPayload(ByteString.copyFrom(payload))
+                                            .setWidth(
+                                                ExtensionDimension.newBuilder()
+                                                    .setLinearDimension(dp(width))
+                                                    .build())
+                                            .setHeight(
+                                                ExtensionDimension.newBuilder()
+                                                    .setLinearDimension(dp(height))
+                                                    .build()))))
+                        .build();
+
+        FrameLayout rootLayout =
+                renderer(
+                    newRendererConfigBuilder(fingerprintedLayout(root))
+                        .setExtensionViewProvider(
+                            (extensionPayload, id) -> {
+                                TextView returnedView = new TextView(getApplicationContext());
+                                returnedView.setText("testing");
+
+                                return returnedView;
+                            }))
+                        .inflate();
+
+        // Check that the outer box is displayed and it has a child.
+        assertThat(rootLayout.getChildCount()).isEqualTo(1);
+
+        ViewGroup boxView = (ViewGroup) rootLayout.getChildAt(0);
+        assertThat(boxView.getChildCount()).isEqualTo(1);
+
+        assertThat(boxView.getMeasuredWidth()).isEqualTo(width);
+        assertThat(boxView.getMeasuredHeight()).isEqualTo(height);
+    }
+
+    @Test
     public void inflate_box_withSemanticsModifier() {
         String textDescription = "this is a button";
         Semantics semantics =
@@ -3601,7 +3649,7 @@
 
     @Test
     public void inflate_extension_onlySpaceIfNoExtension() {
-        byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
+        byte[] payload = "Hello World".getBytes(UTF_8);
         int size = 5;
 
         ExtensionDimension dim =
@@ -3630,7 +3678,7 @@
     public void inflate_rendererExtension_withExtension_callsExtension() {
         List<Pair<byte[], String>> invokedExtensions = new ArrayList<>();
 
-        final byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
+        final byte[] payload = "Hello World".getBytes(UTF_8);
         final int size = 5;
         final String extensionId = "foo";
 
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index 5922761..3c142e4 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -201,7 +201,8 @@
     /**
      * Truncate the text at the last line defined by {@code setMaxLines} in {@link Text} to fit in
      * the {@link Text} element's bounds, but add an ellipsis (i.e. ...) to the end of the text if
-     * it has been truncated.
+     * it has been truncated. Note that this will not add an ellipsis if the number of lines that
+     * fits into the available space is less than the {@code setMaxLines} in {@link Text}.
      *
      * @deprecated Use {@link #TEXT_OVERFLOW_ELLIPSIZE} instead.
      */
@@ -220,14 +221,10 @@
 
     /**
      * Truncate the text to fit in the {@link Text} element's parent bounds, but add an ellipsis
-     * (i.e. ...) to the end of the text if it has been truncated. This will truncate the text even
-     * before {@code setMaxLines} in {@link Text} is reached if there's not enough space in the
-     * parent container. Note that, when this is used, the parent of the {@link Text} element this
-     * corresponds to shouldn't have its width and height set to wrapped, as it can lead to
-     * unexpected results.
+     * (i.e. ...) to the end of the text if it has been truncated.
      *
-     * <p>Note that, on {@link SpanText}, this will behave exactly the same way as
-     * TEXT_OVERFLOW_ELLIPSIZE_END.
+     * <p>Note that, when this is used, the parent of the {@link Text} element this corresponds to
+     * shouldn't have its width and height set to wrapped, as it can lead to unexpected results.
      */
     @RequiresSchemaVersion(major = 1, minor = 300)
     public static final int TEXT_OVERFLOW_ELLIPSIZE = 4;
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index 463b22e..3eda344 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -32,7 +32,7 @@
 
 dependencies {
     implementation(libs.kotlinStdlib)
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation(project(":wear:tiles:tiles"))
 
     api("androidx.wear:wear-tooling-preview:1.0.0")
diff --git a/wear/tiles/tiles-tooling/build.gradle b/wear/tiles/tiles-tooling/build.gradle
index 3ad6fab..73a238c 100644
--- a/wear/tiles/tiles-tooling/build.gradle
+++ b/wear/tiles/tiles-tooling/build.gradle
@@ -15,7 +15,7 @@
 }
 
 dependencies {
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation(project(":wear:tiles:tiles"))
     implementation(project(":wear:tiles:tiles-renderer"))
     implementation(project(":wear:tiles:tiles-tooling-preview"))