Merge "Pin savedState dependencies to alpha01" into androidx-main
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
index 248a8ea..71c7c36 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
@@ -23,8 +23,10 @@
 import android.text.Spanned;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.car.app.CarContext;
 import androidx.car.app.CarToast;
+import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarColor;
@@ -125,6 +127,7 @@
      * Returns the action strip that contains a "bug report" button and "stop navigation" button.
      */
     @NonNull
+    @OptIn(markerClass = ExperimentalCarApi.class)
     public static ActionStrip getActionStrip(
             @NonNull CarContext carContext, @NonNull OnClickListener onStopNavigation) {
         return new ActionStrip.Builder()
@@ -147,6 +150,7 @@
                         new Action.Builder()
                                 .setTitle("Stop")
                                 .setOnClickListener(onStopNavigation)
+                                .setFlags(Action.FLAG_IS_PERSISTENT)
                                 .build())
                 .build();
     }
diff --git a/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt b/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
index bcf427c..c2b190a8 100644
--- a/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
+++ b/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
@@ -453,6 +453,7 @@
     method public static String typeToString(int);
     field public static final androidx.car.app.model.Action APP_ICON;
     field public static final androidx.car.app.model.Action BACK;
+    field @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(5) public static final int FLAG_IS_PERSISTENT = 2; // 0x2
     field @androidx.car.app.annotations.RequiresCarApi(4) public static final int FLAG_PRIMARY = 1; // 0x1
     field public static final androidx.car.app.model.Action PAN;
     field public static final int TYPE_APP_ICON = 65538; // 0x10002
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index bcf427c..c2b190a8 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -453,6 +453,7 @@
     method public static String typeToString(int);
     field public static final androidx.car.app.model.Action APP_ICON;
     field public static final androidx.car.app.model.Action BACK;
+    field @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(5) public static final int FLAG_IS_PERSISTENT = 2; // 0x2
     field @androidx.car.app.annotations.RequiresCarApi(4) public static final int FLAG_PRIMARY = 1; // 0x1
     field public static final androidx.car.app.model.Action PAN;
     field public static final int TYPE_APP_ICON = 65538; // 0x10002
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
index 832ac5e..6f3b005 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Action.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -32,9 +32,11 @@
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.CarContext;
 import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.constraints.CarIconConstraints;
 import androidx.lifecycle.LifecycleOwner;
@@ -86,10 +88,12 @@
      * @hide
      */
     @RestrictTo(LIBRARY)
+    @OptIn(markerClass = ExperimentalCarApi.class)
     @IntDef(
             flag = true,
             value = {
                     FLAG_PRIMARY,
+                    FLAG_IS_PERSISTENT,
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ActionFlag {
@@ -133,6 +137,13 @@
     public static final int FLAG_PRIMARY = 1 << 0;
 
     /**
+     * Indicates that this action will not fade in/out inside an {@link ActionStrip}.
+     */
+    @RequiresCarApi(5)
+    @ExperimentalCarApi
+    public static final int FLAG_IS_PERSISTENT = 1 << 1;
+
+    /**
      * A standard action to show the app's icon.
      *
      * <p>This action is non-interactive.
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index a76c2c1..0e4b541 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -206,5 +206,67 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
   }
 
+  @androidx.compose.runtime.Stable public interface WindowInsets {
+    method public int getBottom(androidx.compose.ui.unit.Density density);
+    method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getTop(androidx.compose.ui.unit.Density density);
+    field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+  }
+
+  public static final class WindowInsets.Companion {
+  }
+
+  public final class WindowInsetsKt {
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+    method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPaddingKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPadding_androidKt {
+    method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+  }
+
+  public final class WindowInsetsSizeKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsets_androidKt {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+  }
+
 }
 
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 11582d2..359b8d2 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -209,5 +209,69 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
   }
 
+  @androidx.compose.runtime.Stable public interface WindowInsets {
+    method public int getBottom(androidx.compose.ui.unit.Density density);
+    method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getTop(androidx.compose.ui.unit.Density density);
+    field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+  }
+
+  public static final class WindowInsets.Companion {
+  }
+
+  public final class WindowInsetsKt {
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+    method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPaddingKt {
+    method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumedWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumedWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues paddingValues);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPadding_androidKt {
+    method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+  }
+
+  public final class WindowInsetsSizeKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsets_androidKt {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+  }
+
 }
 
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 9355fc6..6dc4d15 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -211,5 +211,67 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
   }
 
+  @androidx.compose.runtime.Stable public interface WindowInsets {
+    method public int getBottom(androidx.compose.ui.unit.Density density);
+    method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+    method public int getTop(androidx.compose.ui.unit.Density density);
+    field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+  }
+
+  public static final class WindowInsets.Companion {
+  }
+
+  public final class WindowInsetsKt {
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+    method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+    method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+    method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPaddingKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsetsPadding_androidKt {
+    method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+  }
+
+  public final class WindowInsetsSizeKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+  }
+
+  public final class WindowInsets_androidKt {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+  }
+
 }
 
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 45de895..7a0b162 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -36,11 +36,12 @@
          */
 
         api("androidx.annotation:annotation:1.1.0")
-        api("androidx.compose.ui:ui:1.1.0-rc01")
+        api(project(":compose:ui:ui"))
         api("androidx.compose.ui:ui-unit:1.1.0-rc01")
 
         implementation("androidx.compose.runtime:runtime:1.1.0-rc01")
         implementation("androidx.compose.ui:ui-util:1.0.0")
+        implementation("androidx.core:core:1.7.0")
         implementation(libs.kotlinStdlibCommon)
 
         testImplementation(libs.testRules)
@@ -88,6 +89,7 @@
 
             androidMain.dependencies {
                 api("androidx.annotation:annotation:1.1.0")
+                implementation("androidx.core:core:1.7.0")
             }
 
             desktopMain.dependencies {
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index 2fcf57d..02cf445 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -36,6 +36,8 @@
     implementation(project(":compose:runtime:runtime"))
     implementation("androidx.compose.ui:ui:1.0.0")
     implementation("androidx.compose.ui:ui-text:1.0.0")
+    implementation("androidx.core:core-ktx:1.7.0")
+    implementation("androidx.activity:activity-compose:1.4.0")
 }
 
 androidx {
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
new file mode 100644
index 0000000..0b323e8
--- /dev/null
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2022 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.layout.samples
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.captionBarPadding
+import androidx.compose.foundation.layout.consumedWindowInsets
+import androidx.compose.foundation.layout.displayCutoutPadding
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.layout.mandatorySystemGesturesPadding
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.layout.safeGesturesPadding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.systemGesturesPadding
+import androidx.compose.foundation.layout.union
+import androidx.compose.foundation.layout.waterfallPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+
+@Sampled
+fun captionBarPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.captionBarPadding()) {
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun systemBarsPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.systemBarsPadding()) {
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun displayCutoutPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).statusBarsPadding()) {
+                    Box(Modifier.background(Color.Yellow).displayCutoutPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun statusBarsAndNavigationBarsPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).statusBarsPadding()) {
+                    Box(Modifier.background(Color.Green).navigationBarsPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun imePaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+                    Box(Modifier.imePadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun waterfallPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+                    // The app content shouldn't spill over the edges. They will be green.
+                    Box(Modifier.background(Color.Green).waterfallPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun systemGesturesPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+                    // The app content won't interfere with the system gestures area.
+                    // It will just be white.
+                    Box(Modifier.background(Color.White).systemGesturesPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun mandatorySystemGesturesPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+                    // The app content won't interfere with the mandatory system gestures area.
+                    // It will just be white.
+                    Box(Modifier.background(Color.White).mandatorySystemGesturesPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun safeDrawingPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Black).systemBarsPadding()) {
+                    // The app content won't have anything drawing over it, but all the
+                    // background not in the status bars will be white.
+                    Box(Modifier.background(Color.White).safeDrawingPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun safeGesturesPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Black).systemBarsPadding()) {
+                    // The app content will only be drawn where there is no possible
+                    // gesture confusion. The rest will be plain white
+                    Box(Modifier.background(Color.White).safeGesturesPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun safeContentPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.background(Color.Black).systemBarsPadding()) {
+                    // The app content will only be drawn where there is no possible
+                    // gesture confusion and content will not be drawn over.
+                    // The rest will be plain white
+                    Box(Modifier.background(Color.White).safeContentPadding()) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                val insets = WindowInsets.systemBars.union(WindowInsets.ime)
+                Box(Modifier.background(Color.White).fillMaxSize().windowInsetsPadding(insets)) {
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Sampled
+fun consumedInsetsPaddingSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                with(LocalDensity.current) {
+                    val paddingValues = PaddingValues(horizontal = 20.dp)
+                    Box(
+                        Modifier
+                            .padding(paddingValues)
+                            .consumedWindowInsets(paddingValues)
+                    ) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsInt() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                // Make sure we are at least 10 pixels away from the top.
+                val insets = WindowInsets.statusBars.union(WindowInsets(top = 10))
+                Box(Modifier.windowInsetsPadding(insets)) {
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsDp() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                // Make sure we are at least 10 DP away from the top.
+                val insets = WindowInsets.statusBars.union(WindowInsets(top = 10.dp))
+                Box(Modifier.windowInsetsPadding(insets)) {
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun paddingValuesSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                LazyColumn(
+                    contentPadding = WindowInsets.navigationBars.asPaddingValues()
+                ) {
+                    // items
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Sampled
+fun consumedInsetsSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.padding(WindowInsets.navigationBars.asPaddingValues())) {
+                    Box(Modifier.consumedWindowInsets(WindowInsets.navigationBars)) {
+                        // app content
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt
new file mode 100644
index 0000000..6b875a8
--- /dev/null
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 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.layout.samples
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.windowInsetsEndWidth
+import androidx.compose.foundation.layout.windowInsetsStartWidth
+import androidx.compose.foundation.layout.windowInsetsTopHeight
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.core.view.WindowCompat
+
+@Sampled
+fun insetsStartWidthSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.fillMaxSize()) {
+                    // Background for navigation bar at the start
+                    Box(Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+                        .fillMaxHeight()
+                        .align(Alignment.CenterStart)
+                        .background(Color.Red)
+                    )
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsTopHeightSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.fillMaxSize()) {
+                    // Background for status bar at the top
+                    Box(Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+                        .fillMaxWidth()
+                        .align(Alignment.TopCenter)
+                        .background(Color.Red)
+                    )
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsEndWidthSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.fillMaxSize()) {
+                    // Background for navigation bar at the end
+                    Box(Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+                        .fillMaxHeight()
+                        .align(Alignment.CenterEnd)
+                        .background(Color.Red)
+                    )
+                    // app content
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+fun insetsBottomHeightSample() {
+    class SampleActivity : ComponentActivity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            WindowCompat.setDecorFitsSystemWindows(window, false)
+            super.onCreate(savedInstanceState)
+            setContent {
+                Box(Modifier.fillMaxSize()) {
+                    // Background for navigation bar at the bottom
+                    Box(Modifier.windowInsetsTopHeight(WindowInsets.navigationBars)
+                        .fillMaxWidth()
+                        .align(Alignment.BottomCenter)
+                        .background(Color.Red)
+                    )
+                    // app content
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
new file mode 100644
index 0000000..02fc73a
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
@@ -0,0 +1,872 @@
+/*
+ * Copyright 2022 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.layout
+
+import android.graphics.Insets as FrameworkInsets
+import android.graphics.Rect as AndroidRect
+import android.view.WindowInsets as AndroidWindowInsets
+import android.content.Context
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsetsAnimation
+import android.view.animation.LinearInterpolator
+import android.widget.FrameLayout
+import androidx.activity.ComponentActivity
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets as AndroidXInsets
+import androidx.core.view.DisplayCutoutCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.forEach
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class WindowInsetsPaddingTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    private lateinit var insetsView: InsetsView
+
+    @Before
+    fun setup() {
+        WindowInsetsHolder.setUseTestInsets(true)
+    }
+
+    @After
+    fun teardown() {
+        WindowInsetsHolder.setUseTestInsets(false)
+    }
+
+    @Test
+    fun systemBarsPadding() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.systemBars(),
+            Modifier.systemBarsPadding()
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun displayCutoutPadding() {
+        val coordinates = setInsetContent {
+            Modifier.displayCutoutPadding()
+        }
+
+        val (width, height) = rule.runOnIdle {
+            coordinates.boundsInRoot().bottomRight.round()
+        }
+
+        val insets = sendDisplayCutoutInsets(width, height)
+        insets.assertIsConsumed(WindowInsetsCompat.Type.displayCutout())
+
+        rule.runOnIdle {
+            val expectedRect = Rect(10f, 11f, width - 12f, height - 13f)
+            assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+        }
+    }
+
+    private fun sendDisplayCutoutInsets(width: Int, height: Int): WindowInsetsCompat {
+        val centerWidth = width / 2
+        val centerHeight = height / 2
+
+        val left = AndroidRect(0, centerHeight, 10, centerHeight + 2)
+        val top = AndroidRect(centerWidth, 0, centerWidth + 2, 11)
+        val right = AndroidRect(width - 12, centerHeight, width, centerHeight + 2)
+        val bottom = AndroidRect(centerWidth, height - 13, centerWidth + 2, height)
+        val safeInsets = AndroidXInsets.of(10, 11, 12, 13)
+        val windowInsets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 11, 0, 0))
+            .setInsets(WindowInsetsCompat.Type.displayCutout(), safeInsets)
+            .setDisplayCutout(
+                DisplayCutoutCompat(
+                    safeInsets,
+                    left,
+                    top,
+                    right,
+                    bottom,
+                    AndroidXInsets.of(1, 2, 3, 4)
+                )
+            )
+            .build()
+        return dispatchApplyWindowInsets(windowInsets)
+    }
+
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun statusBarsPaddingApi21() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.statusBars(),
+            Modifier.statusBarsPadding()
+        ) { width, height ->
+            Rect(0f, 11f, width.toFloat(), height.toFloat())
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun statusBarsPaddingApi30() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.statusBars(),
+            Modifier.statusBarsPadding()
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun captionBarPadding() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.captionBar(),
+            Modifier.captionBarPadding()
+        )
+    }
+
+    @Test
+    fun navigationBarsPaddingLeft() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            Modifier.navigationBarsPadding(),
+            sentInsets = AndroidXInsets.of(10, 0, 0, 0)
+        ) { width, height ->
+            Rect(10f, 0f, width.toFloat(), height.toFloat())
+        }
+    }
+
+    @Test
+    fun navigationBarsPaddingRight() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            Modifier.navigationBarsPadding(),
+            sentInsets = AndroidXInsets.of(0, 0, 12, 0)
+        ) { width, height ->
+            Rect(0f, 0f, width - 12f, height.toFloat())
+        }
+    }
+
+    @Test
+    fun navigationBarsPaddingBottom() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            Modifier.navigationBarsPadding(),
+            sentInsets = AndroidXInsets.of(0, 0, 0, 13)
+        ) { width, height ->
+            Rect(0f, 0f, width.toFloat(), height - 13f)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun navigationBarsPaddingApi30() {
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            Modifier.navigationBarsPadding()
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingIme() = testInsetsPadding(WindowInsetsCompat.Type.ime()) {
+        Modifier.windowInsetsPadding(WindowInsets.ime)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingDisplayCutout() = testInsetsPadding(WindowInsetsCompat.Type.displayCutout()) {
+        Modifier.windowInsetsPadding(WindowInsets.displayCutout)
+    }
+
+    @Test
+    fun insetsPaddingStatusBarsTop() = testInsetsPadding(
+        WindowInsetsCompat.Type.statusBars(),
+        sentInsets = AndroidXInsets.of(0, 10, 0, 0),
+        expected = { w, h -> Rect(0f, 10f, w.toFloat(), h.toFloat()) }
+    ) { Modifier.windowInsetsPadding(WindowInsets.statusBars) }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingStatusBarsApi30() = testInsetsPadding(WindowInsetsCompat.Type.statusBars()) {
+        Modifier.windowInsetsPadding(WindowInsets.statusBars)
+    }
+
+    @Test
+    fun insetsPaddingSystemBars() = testInsetsPadding(WindowInsetsCompat.Type.systemBars()) {
+        Modifier.windowInsetsPadding(WindowInsets.systemBars)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun insetsPaddingTappableElement() =
+        testInsetsPadding(WindowInsetsCompat.Type.tappableElement()) {
+            Modifier.windowInsetsPadding(WindowInsets.tappableElement)
+        }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingCaptionBar() = testInsetsPadding(WindowInsetsCompat.Type.captionBar()) {
+        Modifier.windowInsetsPadding(WindowInsets.captionBar)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun insetsPaddingMandatorySystemGestures() =
+        testInsetsPadding(WindowInsetsCompat.Type.mandatorySystemGestures()) {
+            Modifier.windowInsetsPadding(WindowInsets.mandatorySystemGestures)
+        }
+
+    @Test
+    fun insetsPaddingNavigationBarsLeft() =
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            sentInsets = AndroidXInsets.of(10, 0, 0, 0),
+            expected = { width, height -> Rect(10f, 0f, width.toFloat(), height.toFloat()) }
+        ) {
+            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+        }
+
+    @Test
+    fun insetsPaddingNavigationBarsRight() =
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            sentInsets = AndroidXInsets.of(0, 0, 10, 0),
+            expected = { width, height -> Rect(0f, 0f, width - 10f, height.toFloat()) }
+        ) {
+            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+        }
+
+    @Test
+    fun insetsPaddingNavigationBarsBottom() =
+        testInsetsPadding(
+            WindowInsetsCompat.Type.navigationBars(),
+            sentInsets = AndroidXInsets.of(0, 0, 0, 10),
+            expected = { width, height -> Rect(0f, 0f, width.toFloat(), height - 10f) }
+        ) {
+            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+        }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingNavigationBarsApi30() =
+        testInsetsPadding(WindowInsetsCompat.Type.navigationBars()) {
+            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+        }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsPaddingWaterfall() {
+        val coordinates = setInsetContent {
+            Modifier.windowInsetsPadding(WindowInsets.waterfall)
+        }
+
+        val (width, height) = rule.runOnIdle {
+            coordinates.boundsInRoot().bottomRight.round()
+        }
+
+        val insets = sendDisplayCutoutInsets(width, height)
+        insets.assertIsConsumed(WindowInsetsCompat.Type.displayCutout())
+
+        rule.runOnIdle {
+            val expectedRect = Rect(1f, 2f, width - 3f, height - 4f)
+            assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun insetsPaddingSystemGestures() =
+        testInsetsPadding(WindowInsetsCompat.Type.systemGestures()) {
+            Modifier.windowInsetsPadding(WindowInsets.systemGestures)
+        }
+
+    @Test
+    fun mixedInsetsPadding() {
+        val coordinates = setInsetContent {
+            val windowInsets = WindowInsets
+            val insets =
+                windowInsets.navigationBars.union(windowInsets.statusBars).union(windowInsets.ime)
+            Modifier.windowInsetsPadding(insets)
+        }
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 0, 0, 15))
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 10, 0, 0))
+            .setInsets(WindowInsetsCompat.Type.ime(), AndroidXInsets.of(0, 0, 0, 5))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width
+            val height = view.height
+            assertThat(coordinates.boundsInRoot())
+                .isEqualTo(Rect(0f, 10f, width.toFloat(), height - 15f))
+        }
+    }
+
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun consumedInsets() {
+        lateinit var coordinates: LayoutCoordinates
+
+        setContent {
+            with(LocalDensity.current) {
+                CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+                    Box(
+                        Modifier.fillMaxSize().padding(5.toDp(), 4.toDp(), 3.toDp(), 2.toDp())
+                            .consumedWindowInsets(WindowInsets(5, 4, 3, 2))
+                    ) {
+                        Box(Modifier.fillMaxSize().systemBarsPadding()) {
+                            Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+                        }
+                    }
+                }
+            }
+        }
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(10, 11, 12, 13))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width
+            val height = view.height
+            assertThat(coordinates.boundsInRoot())
+                .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+        }
+    }
+
+    @Test
+    fun consumedPadding() {
+        lateinit var coordinates: LayoutCoordinates
+
+        setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+                Box(Modifier.statusBarsPadding()) {
+                    Box(Modifier.systemBarsPadding()) {
+                        Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+                    }
+                }
+            }
+        }
+
+        // wait for layout
+        rule.waitForIdle()
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 5, 0, 0))
+            .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(10, 11, 12, 13))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width
+            val height = view.height
+            assertThat(coordinates.boundsInRoot())
+                .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+        }
+    }
+
+    private fun testInsetsPadding(
+        type: Int,
+        modifier: Modifier,
+        sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13),
+        expected: (Int, Int) -> Rect = { width, height ->
+            Rect(10f, 11f, width - 12f, height - 13f)
+        }
+    ) {
+        testInsetsPadding(type, sentInsets, expected) { modifier }
+    }
+
+    private fun testInsetsPadding(
+        type: Int,
+        sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13),
+        expected: (Int, Int) -> Rect = { width, height ->
+            Rect(10f, 11f, width - 12f, height - 13f)
+        },
+        modifier: @Composable () -> Modifier,
+    ) {
+        val coordinates = setInsetContent(modifier)
+
+        val insets = sendInsets(type, sentInsets)
+        insets.assertIsConsumed(type)
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width
+            val height = view.height
+            val expectedRect = expected(width, height)
+            assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+        }
+    }
+
+    // Removing the last Modifier handling insets should stop insets from being consumed
+    @Test
+    fun removeLastInsetsPadding() {
+        var useStatusBarInsets by mutableStateOf(true)
+        var useNavigationBarInsets by mutableStateOf(true)
+        val coordinates = setInsetContent {
+            (if (useStatusBarInsets) Modifier.statusBarsPadding() else Modifier).then(
+                if (useNavigationBarInsets) Modifier.navigationBarsPadding() else Modifier
+            )
+        }
+
+        rule.runOnIdle {
+            useStatusBarInsets = false
+        }
+
+        sendInsets(WindowInsetsCompat.Type.systemBars())
+            .assertIsConsumed(WindowInsetsCompat.Type.systemBars())
+
+        rule.runOnIdle {
+            useNavigationBarInsets = false
+        }
+
+        sendInsets(WindowInsetsCompat.Type.systemBars())
+            .assertIsNotConsumed(WindowInsetsCompat.Type.systemBars())
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width.toFloat()
+            val height = view.height.toFloat()
+            assertThat(coordinates.boundsInRoot()).isEqualTo(Rect(0f, 0f, width, height))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun animateImeInsets() {
+        with(Api30Methods(rule)) {
+            val coordinates = setInsetContent { Modifier.systemBarsPadding().imePadding() }
+
+            sendInsets(WindowInsetsCompat.Type.systemBars())
+
+            val view = insetsView.findComposeView()
+            val animation = sendImeStart(view)
+
+            val width = view.width
+            val height = view.height
+
+            animation.sendImeProgress(view, 0f)
+
+            rule.runOnIdle {
+                assertThat(coordinates.boundsInRoot())
+                    .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+            }
+
+            animation.sendImeProgress(view, 0.75f)
+
+            rule.runOnIdle {
+                assertThat(coordinates.boundsInRoot())
+                    .isEqualTo(Rect(10f, 11f, width - 12f, height - 15f))
+            }
+
+            animation.sendImeProgress(view, 1f)
+
+            rule.runOnIdle {
+                assertThat(coordinates.boundsInRoot())
+                    .isEqualTo(Rect(10f, 11f, width - 12f, height - 20f))
+            }
+
+            animation.sendImeEnd(view)
+
+            rule.runOnIdle {
+                assertThat(coordinates.boundsInRoot())
+                    .isEqualTo(Rect(10f, 11f, width - 12f, height - 20f))
+            }
+        }
+    }
+
+    @Test
+    fun paddingValues() {
+        lateinit var coordinates: LayoutCoordinates
+
+        setContent {
+            val padding = WindowInsets.systemBars.asPaddingValues()
+            Box(Modifier.fillMaxSize().padding(padding)) {
+                Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+            }
+        }
+
+        // wait for layout
+        rule.waitForIdle()
+
+        val insets = sendInsets(WindowInsetsCompat.Type.systemBars())
+        insets.assertIsConsumed(WindowInsetsCompat.Type.systemBars())
+
+        rule.runOnIdle {
+            val view = insetsView.findComposeView()
+            val width = view.width
+            val height = view.height
+            val expectedRect = Rect(10f, 11f, width - 12f, height - 13f)
+            assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+        }
+    }
+
+    // Each level of the padding should consume some parts of the insets
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun consumeAtEachDepth() {
+        lateinit var statusBar: LayoutCoordinates
+        lateinit var navigationBar: LayoutCoordinates
+        lateinit var ime: LayoutCoordinates
+        setContent {
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .statusBarsPadding()
+                    .onGloballyPositioned { statusBar = it }
+            ) {
+                Box(Modifier.navigationBarsPadding().onGloballyPositioned { navigationBar = it }) {
+                    Box(Modifier.imePadding().fillMaxSize().onGloballyPositioned { ime = it })
+                }
+            }
+        }
+        // wait for layout
+        rule.waitForIdle()
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 10, 0, 0))
+            .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 0, 0, 11))
+            .setInsets(WindowInsetsCompat.Type.ime(), AndroidXInsets.of(0, 10, 0, 20))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val height = insetsView.findComposeView().height
+            assertThat(statusBar.size.height).isEqualTo(height - 10)
+            assertThat(navigationBar.size.height).isEqualTo(height - 21)
+            assertThat(ime.size.height).isEqualTo(height - 30)
+        }
+    }
+
+    // The consumedPaddingInsets() should remove the insets values so that they aren't consumed
+    // further down the hierarchy.
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun consumedInsetsPadding() {
+        lateinit var outer: LayoutCoordinates
+        lateinit var middle: LayoutCoordinates
+        lateinit var inner: LayoutCoordinates
+        setContent {
+            with(LocalDensity.current) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+                        .windowInsetsPadding(WindowInsets(top = 10))
+                        .onGloballyPositioned { outer = it }
+                ) {
+                    Box(Modifier
+                        .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+                        .windowInsetsPadding(WindowInsets(top = 20))
+                        .onGloballyPositioned { middle = it }
+                    ) {
+                        Box(
+                            Modifier
+                                .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+                                .windowInsetsPadding(WindowInsets(top = 30))
+                                .fillMaxSize()
+                                .onGloballyPositioned { inner = it }
+                        )
+                    }
+                }
+            }
+        }
+        // wait for layout
+        rule.waitForIdle()
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 35, 0, 0))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val height = insetsView.findComposeView().height
+            assertThat(outer.size.height).isEqualTo(height - 9)
+            assertThat(middle.size.height).isEqualTo(height - 18)
+            assertThat(inner.size.height).isEqualTo(height - 27)
+        }
+    }
+
+    // The consumedInsets() should remove only values that haven't been consumed.
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun consumedInsetsLimitedConsumption() {
+        lateinit var outer: LayoutCoordinates
+        lateinit var middle: LayoutCoordinates
+        lateinit var inner: LayoutCoordinates
+        setContent {
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .consumedWindowInsets(WindowInsets(top = 1))
+                    .windowInsetsPadding(WindowInsets(top = 10))
+                    .onGloballyPositioned { outer = it }
+            ) {
+                Box(Modifier
+                    .consumedWindowInsets(WindowInsets(top = 10))
+                    .windowInsetsPadding(WindowInsets(top = 20))
+                    .onGloballyPositioned { middle = it }
+                ) {
+                    Box(
+                        Modifier
+                            .consumedWindowInsets(WindowInsets(top = 20))
+                            .windowInsetsPadding(WindowInsets(top = 30))
+                            .fillMaxSize()
+                            .onGloballyPositioned { inner = it }
+                    )
+                }
+            }
+        }
+        // wait for layout
+        rule.waitForIdle()
+
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 35, 0, 0))
+            .build()
+
+        dispatchApplyWindowInsets(insets)
+
+        rule.runOnIdle {
+            val height = insetsView.findComposeView().height
+            assertThat(outer.size.height).isEqualTo(height - 9)
+            assertThat(middle.size.height).isEqualTo(height - 19)
+            assertThat(inner.size.height).isEqualTo(height - 29)
+        }
+    }
+
+    // When the insets change, the layout should be redrawn.
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun newInsetsCausesLayout() {
+        lateinit var coordinates: LayoutCoordinates
+        var useMiddleInsets by mutableStateOf(true)
+
+        setContent {
+            Box(Modifier.fillMaxSize()) {
+                val modifier = if (useMiddleInsets) {
+                    Modifier.consumedWindowInsets(WindowInsets(top = 1))
+                } else {
+                    Modifier.consumedWindowInsets(WindowInsets(top = 2))
+                }
+                with(LocalDensity.current) {
+                    Box(modifier.size(50.toDp())) {
+                        Box(
+                            Modifier
+                                .windowInsetsPadding(WindowInsets(top = 10))
+                                .fillMaxSize()
+                                .onGloballyPositioned { coordinates = it }
+                        )
+                    }
+                }
+            }
+        }
+
+        // wait for layout
+        rule.waitForIdle()
+
+        sendInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 20, 0, 0))
+
+        rule.runOnIdle {
+            assertThat(coordinates.size.height).isEqualTo(41)
+            useMiddleInsets = false
+        }
+
+        rule.runOnIdle {
+            assertThat(coordinates.size.height).isEqualTo(42)
+        }
+    }
+
+    private fun sendInsets(
+        type: Int,
+        sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13)
+    ): WindowInsetsCompat {
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(type, sentInsets)
+            .build()
+        return dispatchApplyWindowInsets(insets)
+    }
+
+    private fun dispatchApplyWindowInsets(insets: WindowInsetsCompat): WindowInsetsCompat {
+        return rule.runOnIdle {
+            val windowInsets = insets.toWindowInsets()!!
+            val view = insetsView
+            insetsView.myInsets = windowInsets
+            val returnedInsets = view.findComposeView().dispatchApplyWindowInsets(windowInsets)
+            WindowInsetsCompat.toWindowInsetsCompat(returnedInsets, view)
+        }
+    }
+
+    private fun setInsetContent(
+        insetsModifier: @Composable () -> Modifier
+    ): LayoutCoordinates {
+        lateinit var coordinates: LayoutCoordinates
+
+        setContent {
+            Box(Modifier.fillMaxSize().background(Color.Blue).then(insetsModifier())) {
+                Box(Modifier.fillMaxSize().onGloballyPositioned {
+                    coordinates = it
+                })
+            }
+        }
+
+        // wait for layout
+        rule.waitForIdle()
+        return coordinates
+    }
+
+    private fun setContent(content: @Composable () -> Unit) {
+        rule.setContent {
+            AndroidView(factory = { context ->
+                val view = InsetsView(context)
+                insetsView = view
+                val composeView = ComposeView(rule.activity)
+                view.addView(
+                    composeView,
+                    ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT
+                    )
+                )
+                composeView.setContent(content)
+                view
+            }, modifier = Modifier.fillMaxSize())
+        }
+    }
+
+    private fun WindowInsetsCompat.assertIsConsumed(type: Int) {
+        val insets = getInsets(type)
+        assertThat(insets).isEqualTo(AndroidXInsets.of(0, 0, 0, 0))
+    }
+
+    private fun WindowInsetsCompat.assertIsNotConsumed(type: Int) {
+        val insets = getInsets(type)
+        assertThat(insets).isNotEqualTo(AndroidXInsets.of(0, 0, 0, 0))
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private class Api30Methods(
+    val rule: AndroidComposeTestRule<ActivityScenarioRule<ComponentActivity>, ComponentActivity>
+) {
+    fun sendImeStart(view: View): WindowInsetsAnimation {
+        return rule.runOnIdle {
+            val animation =
+                WindowInsetsAnimation(AndroidWindowInsets.Type.ime(), LinearInterpolator(), 100L)
+            view.dispatchWindowInsetsAnimationPrepare(animation)
+
+            val imeInsets = FrameworkInsets.of(0, 0, 0, 20)
+            val bounds = WindowInsetsAnimation.Bounds(
+                FrameworkInsets.NONE,
+                imeInsets
+            )
+            view.dispatchWindowInsetsAnimationStart(animation, bounds)
+            animation
+        }
+    }
+
+    fun WindowInsetsAnimation.sendImeProgress(view: View, progress: Float) {
+        return rule.runOnIdle {
+            val bottom = (20 * progress).roundToInt()
+            val imeInsets = FrameworkInsets.of(0, 0, 0, bottom)
+            val systemBarsInsets = FrameworkInsets.of(10, 11, 12, 13)
+            val animatedInsets = AndroidWindowInsets.Builder()
+                .setInsets(AndroidWindowInsets.Type.systemBars(), systemBarsInsets)
+                .setInsets(AndroidWindowInsets.Type.ime(), imeInsets)
+                .build()
+
+            val progressInsets =
+                view.dispatchWindowInsetsAnimationProgress(animatedInsets, listOf(this))
+            assertThat(progressInsets.isConsumed).isTrue()
+        }
+    }
+
+    fun WindowInsetsAnimation.sendImeEnd(view: View) {
+        rule.runOnIdle {
+            view.dispatchWindowInsetsAnimationEnd(this)
+        }
+    }
+}
+
+/**
+ * A View below the compose View that overrides the insets sent by the system. The
+ * compat onApplyWindowInsets listener calls requestApplyInsets(), which results in
+ * the insets being sent again. If we don't override the insets then the system insets
+ * (which are likely 0) will override the insets that we set in the test.
+ */
+internal class InsetsView(context: Context) : FrameLayout(context) {
+    var myInsets: AndroidWindowInsets? = null
+
+    override fun dispatchApplyWindowInsets(insets: AndroidWindowInsets): AndroidWindowInsets {
+        return super.dispatchApplyWindowInsets(myInsets ?: insets)
+    }
+
+    fun findComposeView(): View = findComposeView(this)!!
+
+    private companion object {
+        fun findComposeView(view: View): View? {
+            if (view is ViewRootForTest) {
+                return view
+            } else if (view is ViewGroup) {
+                view.forEach { child ->
+                    val composeView = findComposeView(child)
+                    if (composeView != null) {
+                        return composeView
+                    }
+                }
+            }
+            return null
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
new file mode 100644
index 0000000..e987593
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2022 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.layout
+
+import android.graphics.Rect as AndroidRect
+import android.view.WindowInsets as AndroidWindowInsets
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets as AndroidXInsets
+import androidx.core.view.DisplayCutoutCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class WindowInsetsSizeTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    private lateinit var insetsView: InsetsView
+
+    @Before
+    fun setup() {
+        WindowInsetsHolder.setUseTestInsets(true)
+    }
+
+    @After
+    fun teardown() {
+        WindowInsetsHolder.setUseTestInsets(false)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsStartWidthIme() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.ime(),
+            { Modifier.windowInsetsStartWidth(WindowInsets.ime).fillMaxHeight() },
+            AndroidXInsets.of(10, 0, 0, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsStartWidthImeRtl() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.ime(),
+            { Modifier.windowInsetsStartWidth(WindowInsets.ime).fillMaxHeight() },
+            AndroidXInsets.of(0, 0, 10, 0),
+            LayoutDirection.Rtl
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsEndWidthIme() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.ime(),
+            { Modifier.windowInsetsEndWidth(WindowInsets.ime).fillMaxHeight() },
+            AndroidXInsets.of(0, 0, 10, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsTopHeightIme() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.ime(),
+            { Modifier.windowInsetsTopHeight(WindowInsets.ime).fillMaxWidth() },
+            AndroidXInsets.of(0, 10, 0, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(size.width, 10) }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun insetsBottomHeightIme() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.ime(),
+            { Modifier.windowInsetsBottomHeight(WindowInsets.ime).fillMaxWidth() },
+            AndroidXInsets.of(0, 0, 0, 10),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(size.width, 10) }
+    }
+
+    @Test
+    fun insetsStartWidthNavigationBars() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.navigationBars(),
+            { Modifier.windowInsetsStartWidth(WindowInsets.navigationBars).fillMaxHeight() },
+            AndroidXInsets.of(10, 0, 0, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @Test
+    fun insetsStartWidthNavigationBarsRtl() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.navigationBars(),
+            { Modifier.windowInsetsStartWidth(WindowInsets.navigationBars).fillMaxHeight() },
+            AndroidXInsets.of(0, 0, 10, 0),
+            LayoutDirection.Rtl
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @Test
+    fun insetsEndWidthNavigationBars() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.navigationBars(),
+            { Modifier.windowInsetsEndWidth(WindowInsets.navigationBars).fillMaxHeight() },
+            AndroidXInsets.of(0, 0, 10, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(10, size.height) }
+    }
+
+    @Test
+    fun insetsTopHeightStatusBars() {
+        testInsetsSize(
+            WindowInsetsCompat.Type.statusBars(),
+            { Modifier.windowInsetsTopHeight(WindowInsets.statusBars).fillMaxWidth() },
+            AndroidXInsets.of(0, 10, 0, 0),
+            LayoutDirection.Ltr
+        ) { size -> IntSize(size.width, 10) }
+    }
+
+    @Test
+    fun insetsTopHeightMixed() {
+        val coordinates = setInsetContent(
+            {
+                val insets = WindowInsets
+                Modifier.windowInsetsTopHeight(insets.navigationBars.union(insets.systemBars))
+                    .fillMaxWidth()
+            },
+            LayoutDirection.Ltr
+        )
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 3, 0, 0))
+            .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(0, 10, 0, 0))
+            .build()
+
+        val view = findComposeView()
+        rule.runOnIdle {
+            insetsView.myInsets = insets.toWindowInsets()
+            view.dispatchApplyWindowInsets(insets.toWindowInsets())
+        }
+
+        rule.runOnIdle {
+            assertThat(coordinates.size).isEqualTo(IntSize(view.width, 10))
+        }
+    }
+
+    @Test
+    fun topHeightModifiersAreEqual() {
+        rule.setContent {
+            val modifier1 = Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+            val modifier2 = Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+            assertThat(modifier1).isEqualTo(modifier2)
+        }
+    }
+
+    @Test
+    fun bottomHeightModifiersAreEqual() {
+        rule.setContent {
+            val modifier1 = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)
+            val modifier2 = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)
+            assertThat(modifier1).isEqualTo(modifier2)
+        }
+    }
+
+    @Test
+    fun startWidthModifiersAreEqual() {
+        rule.setContent {
+            val modifier1 = Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+            val modifier2 = Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+            assertThat(modifier1).isEqualTo(modifier2)
+        }
+    }
+
+    @Test
+    fun endWidthModifiersAreEqual() {
+        rule.setContent {
+            val modifier1 = Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+            val modifier2 = Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+            assertThat(modifier1).isEqualTo(modifier2)
+        }
+    }
+
+    private fun testInsetsSize(
+        type: Int,
+        modifier: @Composable () -> Modifier,
+        sentInsets: AndroidXInsets,
+        layoutDirection: LayoutDirection,
+        expected: (IntSize) -> IntSize
+    ) {
+        val coordinates = setInsetContent(modifier, layoutDirection)
+
+        val insets = sendInsets(type, sentInsets)
+        assertThat(insets.isConsumed)
+
+        rule.runOnIdle {
+            val view = findComposeView()
+            val width = view.width
+            val height = view.height
+            val expectedSize = expected(IntSize(width, height))
+            assertThat(coordinates.size).isEqualTo(expectedSize)
+        }
+    }
+
+    private fun sendInsets(
+        type: Int,
+        sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13)
+    ): AndroidWindowInsets {
+        val builder = WindowInsetsCompat.Builder()
+            .setInsets(type, sentInsets)
+        if (type == WindowInsetsCompat.Type.displayCutout()) {
+            val view = findComposeView()
+            val width = view.width
+            val height = view.height
+            val safeRect = AndroidRect(0, 0, width, height)
+            val cutoutRect =
+                AndroidRect(width / 2 - 5, height / 2 - 5, width / 2 + 5, height / 2 + 5)
+            when {
+                sentInsets.left > 0 -> {
+                    safeRect.left = sentInsets.left
+                    cutoutRect.left = 0
+                    cutoutRect.right = safeRect.left
+                }
+                sentInsets.top > 0 -> {
+                    safeRect.top = sentInsets.top
+                    cutoutRect.top = 0
+                    cutoutRect.bottom = safeRect.top
+                }
+                sentInsets.right > 0 -> {
+                    safeRect.right = width - sentInsets.right
+                    cutoutRect.right = width
+                    cutoutRect.left = width - safeRect.right
+                }
+                sentInsets.bottom > 0 -> {
+                    safeRect.bottom = sentInsets.bottom
+                    cutoutRect.bottom = height
+                    cutoutRect.top = height - safeRect.bottom
+                }
+            }
+            builder.setDisplayCutout(DisplayCutoutCompat(safeRect, listOf(cutoutRect)))
+        }
+        val insets = WindowInsetsCompat.Builder()
+            .setInsets(type, sentInsets)
+            .build()
+        insetsView.myInsets = insets.toWindowInsets()
+        return rule.runOnIdle {
+            AndroidWindowInsets(
+                findComposeView().dispatchApplyWindowInsets(insets.toWindowInsets())
+            )
+        }
+    }
+
+    private fun setInsetContent(
+        sizeModifier: @Composable () -> Modifier,
+        layoutDirection: LayoutDirection
+    ): LayoutCoordinates {
+        lateinit var coordinates: LayoutCoordinates
+
+        rule.setContent {
+            AndroidView(factory = { context ->
+                val view = InsetsView(context)
+                insetsView = view
+                val composeView = ComposeView(rule.activity)
+                view.addView(
+                    composeView,
+                    ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT
+                    )
+                )
+                composeView.setContent {
+                    CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                        Box(Modifier.wrapContentSize().onGloballyPositioned { coordinates = it }) {
+                            Box(sizeModifier())
+                        }
+                    }
+                }
+                view
+            }, modifier = Modifier.fillMaxSize())
+        }
+
+        // wait for layout
+        rule.waitForIdle()
+        return coordinates
+    }
+
+    private fun findComposeView(): View = insetsView.findComposeView()
+}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
new file mode 100644
index 0000000..bfc442d
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2022 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.layout
+
+import androidx.core.graphics.Insets as AndroidXInsets
+import android.os.Build
+import android.view.View
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+import java.util.WeakHashMap
+import org.jetbrains.annotations.TestOnly
+
+internal fun AndroidXInsets.toInsetsValues(): InsetsValues =
+    InsetsValues(left, top, right, bottom)
+
+internal fun ValueInsets(insets: AndroidXInsets, name: String): ValueInsets =
+    ValueInsets(insets.toInsetsValues(), name)
+
+/**
+ * For the [WindowInsetsCompat.Type.captionBar].
+ */
+val WindowInsets.Companion.captionBar: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().captionBar
+
+/**
+ * For the [WindowInsetsCompat.Type.displayCutout]. This insets represents the area that the
+ * display cutout (e.g. for camera) is and important content should be excluded from.
+ */
+val WindowInsets.Companion.displayCutout: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().displayCutout
+
+/**
+ * For the [WindowInsetsCompat.Type.ime]. On [Build.VERSION_CODES.R] and above, the
+ * soft keyboard can be detected and [ime] will animate when it shows.
+ *
+ * Developers should set `android:windowSoftInputMode="adjustResize"` in their
+ * `AndroidManifest.xml` file and call `WindowCompat.setDecorFitsSystemWindows(window, false)`
+ * in their [android.app.Activity.onCreate].
+ */
+val WindowInsets.Companion.ime: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().ime
+
+/**
+ * For the [WindowInsetsCompat.Type.mandatorySystemGestures]. These insets represents the
+ * space where system gestures have priority over application gestures.
+ */
+val WindowInsets.Companion.mandatorySystemGestures: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().mandatorySystemGestures
+
+/**
+ * For the [WindowInsetsCompat.Type.navigationBars]. These insets represent where
+ * system UI places navigation bars. Interactive UI should avoid the navigation bars
+ * area.
+ */
+val WindowInsets.Companion.navigationBars: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().navigationBars
+
+/**
+ * For the [WindowInsetsCompat.Type.statusBars].
+ */
+val WindowInsets.Companion.statusBars: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().statusBars
+
+/**
+ * For the [WindowInsetsCompat.Type.systemBars].
+ */
+val WindowInsets.Companion.systemBars: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().systemBars
+
+/**
+ * For the [WindowInsetsCompat.Type.systemGestures].
+ */
+val WindowInsets.Companion.systemGestures: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().systemGestures
+
+/**
+ * For the [WindowInsetsCompat.Type.tappableElement].
+ */
+val WindowInsets.Companion.tappableElement: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().tappableElement
+
+/**
+ * The insets for the curved areas in a waterfall display.
+ */
+val WindowInsets.Companion.waterfall: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().waterfall
+
+/**
+ * The insets that include areas where content may be covered by other drawn content.
+ * This includes all [system bars][systemBars], [display cutout][displayCutout], and
+ * [soft keyboard][ime].
+ */
+val WindowInsets.Companion.safeDrawing: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().safeDrawing
+
+/**
+ * The insets that include areas where gestures may be confused with other input,
+ * including [system gestures][systemGestures],
+ * [mandatory system gestures][mandatorySystemGestures],
+ * [rounded display areas][waterfall], and [tappable areas][tappableElement].
+ */
+val WindowInsets.Companion.safeGestures: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().safeGestures
+
+/**
+ * The insets that include all areas that may be drawn over or have gesture confusion,
+ * including everything in [safeDrawing] and [safeGestures].
+ */
+val WindowInsets.Companion.safeContent: WindowInsets
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().safeContent
+
+/**
+ * The insets for various values in the current window.
+ */
+internal class WindowInsetsHolder private constructor(insets: WindowInsetsCompat?) {
+    val captionBar =
+        valueInsets(insets, WindowInsetsCompat.Type.captionBar(), "captionBar")
+    val displayCutout =
+        valueInsets(insets, WindowInsetsCompat.Type.displayCutout(), "displayCutout")
+    val ime = valueInsets(insets, WindowInsetsCompat.Type.ime(), "ime")
+    val mandatorySystemGestures = valueInsets(
+        insets,
+        WindowInsetsCompat.Type.mandatorySystemGestures(),
+        "mandatorySystemGestures"
+    )
+    val navigationBars =
+        valueInsets(insets, WindowInsetsCompat.Type.navigationBars(), "navigationBars")
+    val statusBars =
+        valueInsets(insets, WindowInsetsCompat.Type.statusBars(), "statusBars")
+    val systemBars =
+        valueInsets(insets, WindowInsetsCompat.Type.systemBars(), "systemBars")
+    val systemGestures =
+        valueInsets(insets, WindowInsetsCompat.Type.systemGestures(), "systemGestures")
+    val tappableElement =
+        valueInsets(insets, WindowInsetsCompat.Type.tappableElement(), "tappableElement")
+    val waterfall =
+        ValueInsets(insets?.displayCutout?.waterfallInsets ?: AndroidXInsets.NONE, "waterfall")
+    val safeDrawing =
+        systemBars.union(ime).union(displayCutout)
+    val safeGestures: WindowInsets =
+        tappableElement.union(mandatorySystemGestures).union(systemGestures).union(waterfall)
+    val safeContent: WindowInsets = safeDrawing.union(safeGestures)
+
+    /**
+     * The number of accesses to [WindowInsetsHolder]. When this reaches
+     * zero, the listeners are removed. When it increases to 1, the listeners are added.
+     */
+    private var consumers = 0
+
+    private val insetsListener = InsetsListener(this)
+
+    /**
+     * A usage of [WindowInsetsHolder.current] was added. We must track so that when the
+     * first one is added, listeners are set and when the last is removed, the listeners
+     * are removed.
+     */
+    fun incrementConsumers(view: View) {
+        if (consumers == 0) {
+            // add listeners
+            ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener)
+
+            // We don't need animation callbacks on earlier versions, so don't bother adding
+            // the listener. ViewCompat calls the animation callbacks superfluously.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                ViewCompat.setWindowInsetsAnimationCallback(view, insetsListener)
+            }
+        }
+        consumers++
+    }
+
+    /**
+     * A usage of [WindowInsetsHolder.current] was removed. We must track so that when the
+     * first one is added, listeners are set and when the last is removed, the listeners
+     * are removed.
+     */
+    fun decrementConsumers(view: View) {
+        consumers--
+        if (consumers == 0) {
+            // remove listeners
+            ViewCompat.setOnApplyWindowInsetsListener(view, null)
+            ViewCompat.setWindowInsetsAnimationCallback(view, null)
+        }
+    }
+
+    /**
+     * Updates the WindowInsets values and notifies changes.
+     */
+    fun update(windowInsets: WindowInsetsCompat) {
+        Snapshot.withMutableSnapshot {
+            val insets = if (testInsets) {
+                // WindowInsetsCompat erases insets that aren't part of the device.
+                // For example, if there is no navigation bar because of hardware keys,
+                // the bottom navigation bar will be removed. By using the constructor
+                // that doesn't accept a View, it doesn't remove the insets that aren't
+                // possible. This is important for testing on arbitrary hardware.
+                WindowInsetsCompat.toWindowInsetsCompat(windowInsets.toWindowInsets()!!)
+            } else {
+                windowInsets
+            }
+            captionBar.value =
+                insets.getInsets(WindowInsetsCompat.Type.captionBar()).toInsetsValues()
+            ime.value =
+                insets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
+            displayCutout.value =
+                insets.getInsets(WindowInsetsCompat.Type.displayCutout()).toInsetsValues()
+            navigationBars.value =
+                insets.getInsets(WindowInsetsCompat.Type.navigationBars()).toInsetsValues()
+            statusBars.value =
+                insets.getInsets(WindowInsetsCompat.Type.statusBars()).toInsetsValues()
+            systemBars.value =
+                insets.getInsets(WindowInsetsCompat.Type.systemBars()).toInsetsValues()
+            systemGestures.value =
+                insets.getInsets(WindowInsetsCompat.Type.systemGestures()).toInsetsValues()
+            tappableElement.value =
+                insets.getInsets(WindowInsetsCompat.Type.tappableElement()).toInsetsValues()
+            mandatorySystemGestures.value =
+                insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()).toInsetsValues()
+
+            val cutout = insets.displayCutout
+            if (cutout != null) {
+                val waterfallInsets = cutout.waterfallInsets
+                waterfall.value = waterfallInsets.toInsetsValues()
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * A mapping of AndroidComposeView to ComposeWindowInsets. Normally a tag is a great
+         * way to do this mapping, but off-UI thread and multithreaded composition don't
+         * allow using the tag.
+         */
+        private val viewMap = WeakHashMap<View, WindowInsetsHolder>()
+
+        private var testInsets = false
+
+        /**
+         * Testing Window Insets is difficult, so we have this to help eliminate device-specifics
+         * from the WindowInsets. This is indirect because `@TestOnly` cannot be applied to a
+         * property with a backing field.
+         */
+        @TestOnly
+        fun setUseTestInsets(testInsets: Boolean) {
+            this.testInsets = testInsets
+        }
+
+        @Composable
+        fun current(): WindowInsetsHolder {
+            val view = LocalView.current
+            val insets = getOrCreateFor(view)
+
+            DisposableEffect(insets) {
+                insets.incrementConsumers(view)
+                onDispose {
+                    insets.decrementConsumers(view)
+                }
+            }
+            return insets
+        }
+
+        /**
+         * Returns the [WindowInsetsHolder] associated with [view] or creates one and associates
+         * it.
+         */
+        private fun getOrCreateFor(view: View): WindowInsetsHolder {
+            return synchronized(viewMap) {
+                viewMap.getOrPut(view) {
+                    val insets = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                        RootWindowInsetsApi23.rootWindowInsets(view)
+                    } else {
+                        null
+                    }
+                    WindowInsetsHolder(insets)
+                }
+            }
+        }
+
+        /**
+         * Creates a [ValueInsets] using the value from [windowInsets] if it isn't `null`
+         */
+        private fun valueInsets(
+            windowInsets: WindowInsetsCompat?,
+            type: Int,
+            name: String
+        ): ValueInsets {
+            val initial = windowInsets?.getInsets(type) ?: AndroidXInsets.NONE
+            return ValueInsets(initial, name)
+        }
+    }
+}
+
+/**
+ * Used to get the [View.getRootWindowInsets] only on M and above
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+private object RootWindowInsetsApi23 {
+    @DoNotInline
+    fun rootWindowInsets(view: View): WindowInsetsCompat? {
+        return view.rootWindowInsets?.let {
+            WindowInsetsCompat.toWindowInsetsCompat(it, view)
+        }
+    }
+}
+
+private class InsetsListener(
+    val composeInsets: WindowInsetsHolder,
+) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP), OnApplyWindowInsetsListener {
+
+    override fun onProgress(
+        insets: WindowInsetsCompat,
+        runningAnimations: MutableList<WindowInsetsAnimationCompat>
+    ): WindowInsetsCompat {
+        composeInsets.update(insets)
+        return WindowInsetsCompat.CONSUMED
+    }
+
+    override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+        composeInsets.update(insets)
+        return WindowInsetsCompat.CONSUMED
+    }
+}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
new file mode 100644
index 0000000..244bf86
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2022 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.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * Adds padding to accommodate the [safe drawing][WindowInsets.Companion.safeDrawing] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeDrawing] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the area that the parent
+ * pads for the status bars will not be padded again by this [safeDrawingPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeDrawingPaddingSample
+ */
+fun Modifier.safeDrawingPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "safeDrawingPadding" }) { safeDrawing }
+
+/**
+ * Adds padding to accommodate the [safe gestures][WindowInsets.Companion.safeGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [safeGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeGesturesPaddingSample
+ */
+fun Modifier.safeGesturesPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "safeGesturesPadding" }) { safeGestures }
+
+/**
+ * Adds padding to accommodate the [safe content][WindowInsets.Companion.safeContent] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeContent] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [safeContentPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeContentPaddingSample
+ */
+fun Modifier.safeContentPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "safeContentPadding" }) { safeContent }
+
+/**
+ * Adds padding to accommodate the [system bars][WindowInsets.Companion.systemBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.systemBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [systemBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.systemBarsPaddingSample
+ */
+fun Modifier.systemBarsPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "systemBarsPadding" }) { systemBars }
+
+/**
+ * Adds padding to accommodate the [display cutout][WindowInsets.Companion.displayCutout].
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.displayCutout] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [displayCutoutPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.displayCutoutPaddingSample
+ */
+fun Modifier.displayCutoutPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "displayCutoutPadding" }) { displayCutout }
+
+/**
+ * Adds padding to accommodate the [status bars][WindowInsets.Companion.statusBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.statusBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [displayCutoutPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [statusBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.statusBarsAndNavigationBarsPaddingSample
+ */
+fun Modifier.statusBarsPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "statusBarsPadding" }) { statusBars }
+
+/**
+ * Adds padding to accommodate the [ime][WindowInsets.Companion.ime] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.ime] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [imePadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.imePaddingSample
+ */
+fun Modifier.imePadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "imePadding" }) { ime }
+
+/**
+ * Adds padding to accommodate the [navigation bars][WindowInsets.Companion.navigationBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.navigationBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [systemBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [navigationBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.statusBarsAndNavigationBarsPaddingSample
+ */
+fun Modifier.navigationBarsPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "navigationBarsPadding" }) { navigationBars }
+
+/**
+ * Adds padding to accommodate the [caption bar][WindowInsets.Companion.captionBar] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.captionBar] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [displayCutoutPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [captionBarPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.captionBarPaddingSample
+ */
+fun Modifier.captionBarPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "captionBarPadding" }) { captionBar }
+
+/**
+ * Adds padding to accommodate the [waterfall][WindowInsets.Companion.waterfall] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.waterfall] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [systemGesturesPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [waterfallPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.waterfallPaddingSample
+ */
+fun Modifier.waterfallPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "waterfallPadding" }) { waterfall }
+
+/**
+ * Adds padding to accommodate the [system gestures][WindowInsets.Companion.systemGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.systemGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [waterfallPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [systemGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.systemGesturesPaddingSample
+ */
+fun Modifier.systemGesturesPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "systemGesturesPadding" }) { systemGestures }
+
+/**
+ * Adds padding to accommodate the
+ * [mandatory system gestures][WindowInsets.Companion.mandatorySystemGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.mandatorySystemGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [mandatorySystemGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.mandatorySystemGesturesPaddingSample
+ */
+fun Modifier.mandatorySystemGesturesPadding() =
+    windowInsetsPadding(debugInspectorInfo { name = "mandatorySystemGesturesPadding" }) {
+        mandatorySystemGestures
+    }
+
+@Suppress("NOTHING_TO_INLINE", "ModifierInspectorInfo")
+@Stable
+private inline fun Modifier.windowInsetsPadding(
+    noinline inspectorInfo: InspectorInfo.() -> Unit,
+    crossinline insetsCalculation: WindowInsetsHolder.() -> WindowInsets
+): Modifier = composed(inspectorInfo) {
+    val composeInsets = WindowInsetsHolder.current()
+    remember(composeInsets) {
+        val insets = composeInsets.insetsCalculation()
+        InsetsPaddingModifier(insets)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt
new file mode 100644
index 0000000..627e0f5
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2022 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.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+
+/**
+ * A representation of window insets that tracks access to enable recomposition,
+ * relayout, and redrawing when values change. These values should not be read during composition
+ * to avoid doing composition for every frame of an animation. Use methods like
+ * [Modifier.windowInsetsPadding], [Modifier.systemBarsPadding], and
+ * [Modifier.windowInsetsTopHeight] for Modifiers that will not cause recomposition when values
+ * change.
+ *
+ * Use the [WindowInsets.Companion] extensions to retrieve [WindowInsets] for the current
+ * window.
+ */
+@Stable
+interface WindowInsets {
+    /**
+     * The space, in pixels, at the left of the window that the inset represents.
+     */
+    fun getLeft(density: Density, layoutDirection: LayoutDirection): Int
+
+    /**
+     * The space, in pixels, at the top of the window that the inset represents.
+     */
+    fun getTop(density: Density): Int
+
+    /**
+     * The space, in pixels, at the right of the window that the inset represents.
+     */
+    fun getRight(density: Density, layoutDirection: LayoutDirection): Int
+
+    /**
+     * The space, in pixels, at the bottom of the window that the inset represents.
+     */
+    fun getBottom(density: Density): Int
+
+    companion object
+}
+
+/**
+ * Returns an [WindowInsets] that has the maximum values of this [WindowInsets] and [insets].
+ */
+fun WindowInsets.union(insets: WindowInsets): WindowInsets = UnionInsets(this, insets)
+
+/**
+ * Returns the values in this [WindowInsets] that are not also in [insets]. For example, if this
+ * [WindowInsets] has a [WindowInsets.getTop] value of `10` and [insets] has a
+ * [WindowInsets.getTop] value of `8`, the returned [WindowInsets] will have a
+ * [WindowInsets.getTop] value of `2`.
+ *
+ * Negative values are never returned. For example if [insets] has a [WindowInsets.getTop] of `10`
+ * and this has a [WindowInsets.getTop] of `0`, the returned [WindowInsets] will have a
+ * [WindowInsets.getTop] value of `0`.
+ */
+fun WindowInsets.exclude(insets: WindowInsets): WindowInsets = ExcludeInsets(this, insets)
+
+/**
+ * Returns the an [WindowInsets] that has values of this, added to the values of [insets].
+ * For example, if this has a top of 10 and insets has a top of 5, the returned [WindowInsets]
+ * will have a top of 15.
+ */
+fun WindowInsets.add(insets: WindowInsets): WindowInsets = AddedInsets(this, insets)
+
+/**
+ * Convert an [WindowInsets] to a [PaddingValues] and uses [LocalDensity] for DP to pixel conversion.
+ * [PaddingValues] can be passed to some containers to pad internal content so that it doesn't
+ * overlap the insets when fully scrolled. Ensure that the insets are [consumed][consumedWindowInsets]
+ * after the padding is applied if insets are to be used further down the hierarchy.
+ *
+ * @sample androidx.compose.foundation.layout.samples.paddingValuesSample
+ */
+@Composable
+fun WindowInsets.asPaddingValues(): PaddingValues = InsetsPaddingValues(this, LocalDensity.current)
+
+/**
+ * Convert a [PaddingValues] to an [WindowInsets].
+ */
+internal fun PaddingValues.asInsets(): WindowInsets = PaddingValuesInsets(this)
+
+/**
+ * Create an [WindowInsets] with fixed dimensions.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsInt
+ */
+fun WindowInsets(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0): WindowInsets =
+    FixedIntInsets(left, top, right, bottom)
+
+/**
+ * Create an [WindowInsets] with fixed dimensions, using [Dp] values.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsDp
+ */
+fun WindowInsets(
+    left: Dp = 0.dp,
+    top: Dp = 0.dp,
+    right: Dp = 0.dp,
+    bottom: Dp = 0.dp
+): WindowInsets = FixedDpInsets(left, top, right, bottom)
+
+@Immutable
+private class FixedIntInsets(
+    private val leftVal: Int,
+    private val topVal: Int,
+    private val rightVal: Int,
+    private val bottomVal: Int
+) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = leftVal
+    override fun getTop(density: Density): Int = topVal
+    override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = rightVal
+    override fun getBottom(density: Density): Int = bottomVal
+
+    override fun toString(): String {
+        return "Insets(left=$leftVal, top=$topVal, right=$rightVal, bottom=$bottomVal)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is FixedIntInsets) {
+            return false
+        }
+
+        return leftVal == other.leftVal && topVal == other.topVal &&
+            rightVal == other.rightVal && bottomVal == other.bottomVal
+    }
+
+    override fun hashCode(): Int {
+        var result = leftVal
+        result = 31 * result + topVal
+        result = 31 * result + rightVal
+        result = 31 * result + bottomVal
+        return result
+    }
+}
+
+@Immutable
+private class FixedDpInsets(
+    private val leftDp: Dp,
+    private val topDp: Dp,
+    private val rightDp: Dp,
+    private val bottomDp: Dp
+) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+        with(density) { leftDp.roundToPx() }
+
+    override fun getTop(density: Density) = with(density) { topDp.roundToPx() }
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+        with(density) { rightDp.roundToPx() }
+    override fun getBottom(density: Density) = with(density) { bottomDp.roundToPx() }
+
+    override fun toString(): String {
+        return "Insets(left=$leftDp, top=$topDp, right=$rightDp, bottom=$bottomDp)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is FixedDpInsets) {
+            return false
+        }
+
+        return leftDp == other.leftDp && topDp == other.topDp &&
+            rightDp == other.rightDp && bottomDp == other.bottomDp
+    }
+
+    override fun hashCode(): Int {
+        var result = leftDp.hashCode()
+        result = 31 * result + topDp.hashCode()
+        result = 31 * result + rightDp.hashCode()
+        result = 31 * result + bottomDp.hashCode()
+        return result
+    }
+}
+
+/**
+ * An [WindowInsets] that comes straight from [androidx.core.graphics.Insets], whose value can
+ * be updated.
+ */
+@Stable
+internal class ValueInsets(val insets: InsetsValues, val name: String) : WindowInsets {
+    internal var value by mutableStateOf(insets)
+
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = value.left
+    override fun getTop(density: Density) = value.top
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) = value.right
+    override fun getBottom(density: Density) = value.bottom
+
+    override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+        if (other !is ValueInsets) {
+            return false
+        }
+        return value == other.value
+    }
+
+    override fun hashCode(): Int {
+        return name.hashCode()
+    }
+
+    override fun toString(): String {
+        return "$name(left=${insets.left}, top=${insets.top}, " +
+            "right=${insets.right}, bottom=${insets.bottom})"
+    }
+}
+
+@Immutable
+internal class InsetsValues(val left: Int, val top: Int, val right: Int, val bottom: Int) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is InsetsValues) {
+            return false
+        }
+
+        return left == other.left &&
+            top == other.top &&
+            right == other.right &&
+            bottom == other.bottom
+    }
+
+    override fun hashCode(): Int {
+        var result = left
+        result = 31 * result + top
+        result = 31 * result + right
+        result = 31 * result + bottom
+        return result
+    }
+}
+
+/**
+ * An [WindowInsets] that includes the maximum value of [first] and [second] as returned from
+ * [WindowInsets.union].
+ */
+@Stable
+private class UnionInsets(
+    private val first: WindowInsets,
+    private val second: WindowInsets
+) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+        maxOf(first.getLeft(density, layoutDirection), second.getLeft(density, layoutDirection))
+
+    override fun getTop(density: Density) =
+        maxOf(first.getTop(density), second.getTop(density))
+
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+        maxOf(first.getRight(density, layoutDirection), second.getRight(density, layoutDirection))
+
+    override fun getBottom(density: Density) =
+        maxOf(first.getBottom(density), second.getBottom(density))
+
+    override fun hashCode(): Int = first.hashCode() + second.hashCode() * 31
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is UnionInsets) {
+            return false
+        }
+        return other.first == first && other.second == second
+    }
+
+    override fun toString(): String = "($first ∪ $second)"
+}
+
+/**
+ * An [WindowInsets] that includes the added value of [first] to [second].
+ */
+@Stable
+private class AddedInsets(
+    private val first: WindowInsets,
+    private val second: WindowInsets
+) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+        first.getLeft(density, layoutDirection) + second.getLeft(density, layoutDirection)
+
+    override fun getTop(density: Density) =
+        first.getTop(density) + second.getTop(density)
+
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+        first.getRight(density, layoutDirection) + second.getRight(density, layoutDirection)
+
+    override fun getBottom(density: Density) =
+        first.getBottom(density) + second.getBottom(density)
+
+    override fun hashCode(): Int = first.hashCode() + second.hashCode() * 31
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is AddedInsets) {
+            return false
+        }
+        return other.first == first && other.second == second
+    }
+
+    override fun toString(): String = "($first + $second)"
+}
+
+/**
+ * An [WindowInsets] that includes the value of [included] that is not included in [excluded] as
+ * returned from [WindowInsets.exclude].
+ */
+@Stable
+private class ExcludeInsets(
+    private val included: WindowInsets,
+    private val excluded: WindowInsets
+) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+        (included.getLeft(density, layoutDirection) - excluded.getLeft(density, layoutDirection))
+            .coerceAtLeast(0)
+
+    override fun getTop(density: Density) =
+        (included.getTop(density) - excluded.getTop(density)).coerceAtLeast(0)
+
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+        (included.getRight(density, layoutDirection) - excluded.getRight(density, layoutDirection))
+            .coerceAtLeast(0)
+
+    override fun getBottom(density: Density) =
+        (included.getBottom(density) - excluded.getBottom(density)).coerceAtLeast(0)
+
+    override fun toString(): String = "($included - $excluded)"
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is ExcludeInsets) {
+            return false
+        }
+
+        return (other.included == included && other.excluded == excluded)
+    }
+
+    override fun hashCode(): Int = 31 * included.hashCode() + excluded.hashCode()
+}
+
+/**
+ * An [WindowInsets] calculated from [paddingValues].
+ */
+@Stable
+private class PaddingValuesInsets(private val paddingValues: PaddingValues) : WindowInsets {
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection) = with(density) {
+        paddingValues.calculateLeftPadding(layoutDirection).roundToPx()
+    }
+
+    override fun getTop(density: Density) = with(density) {
+        paddingValues.calculateTopPadding().roundToPx()
+    }
+
+    override fun getRight(density: Density, layoutDirection: LayoutDirection) = with(density) {
+        paddingValues.calculateRightPadding(layoutDirection).roundToPx()
+    }
+
+    override fun getBottom(density: Density) = with(density) {
+        paddingValues.calculateBottomPadding().roundToPx()
+    }
+
+    override fun toString(): String {
+        val layoutDirection = LayoutDirection.Ltr
+        val start = paddingValues.calculateLeftPadding(layoutDirection)
+        val top = paddingValues.calculateTopPadding()
+        val end = paddingValues.calculateRightPadding(layoutDirection)
+        val bottom = paddingValues.calculateBottomPadding()
+        return "PaddingValues($start, $top, $end, $bottom)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is PaddingValuesInsets) {
+            return false
+        }
+
+        return other.paddingValues == paddingValues
+    }
+
+    override fun hashCode(): Int = paddingValues.hashCode()
+}
+
+@Stable
+private class InsetsPaddingValues(
+    val insets: WindowInsets,
+    private val density: Density
+) : PaddingValues {
+    override fun calculateLeftPadding(layoutDirection: LayoutDirection) = with(density) {
+        insets.getLeft(this, layoutDirection).toDp()
+    }
+
+    override fun calculateTopPadding() = with(density) {
+        insets.getTop(this).toDp()
+    }
+
+    override fun calculateRightPadding(layoutDirection: LayoutDirection) = with(density) {
+        insets.getRight(this, layoutDirection).toDp()
+    }
+
+    override fun calculateBottomPadding() = with(density) {
+        insets.getBottom(this).toDp()
+    }
+
+    override fun toString(): String {
+        return "InsetsPaddingValues(insets=$insets, density=$density)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is InsetsPaddingValues) {
+            return false
+        }
+        return insets == other.insets && density == other.density
+    }
+
+    override fun hashCode(): Int {
+        var result = insets.hashCode()
+        result = 31 * result + density.hashCode()
+        return result
+    }
+}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
new file mode 100644
index 0000000..5fbb82fc
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 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.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+
+/**
+ * Adds padding so that the content doesn't enter [insets] space.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent
+ * layout will be excluded from [insets]. [insets] will be [consumed][consumedWindowInsets] for
+ * child layouts as well.
+ *
+ * For example, if an ancestor uses [statusBarsPadding] and this modifier uses
+ * [WindowInsets.Companion.systemBars], the portion of the system bars that the status bars uses
+ * will not be padded again by this modifier.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsPaddingSample
+ * @see WindowInsets
+ */
+@Stable
+fun Modifier.windowInsetsPadding(insets: WindowInsets): Modifier = this.then(
+    InsetsPaddingModifier(insets, debugInspectorInfo {
+        name = "windowInsetsPadding"
+        properties["insets"] = insets
+    })
+)
+
+/**
+ * Consume insets that haven't been consumed yet by other insets Modifiers similar to
+ * [windowInsetsPadding] without adding any padding.
+ *
+ * This can be useful when content offsets are provided by [WindowInsets.asPaddingValues].
+ * This should be used further down the hierarchy than the [PaddingValues] is used so
+ * that the values aren't consumed before the padding is added.
+ *
+ * @sample androidx.compose.foundation.layout.samples.consumedInsetsSample
+ */
+@ExperimentalLayoutApi
+@Stable
+fun Modifier.consumedWindowInsets(insets: WindowInsets): Modifier = this.then(
+    UnionInsetsConsumingModifier(insets, debugInspectorInfo {
+        name = "consumedWindowInsets"
+        properties["insets"] = insets
+    })
+)
+
+/**
+ * Consume [paddingValues] as insets as if the padding was added irrespective of insets.
+ * Layouts further down the hierarchy that use [windowInsetsPadding], [safeContentPadding],
+ * and other insets padding Modifiers won't pad for the values that [paddingValues] provides.
+ * This can be useful when content offsets are provided by layout rather than [windowInsetsPadding]
+ * modifiers.
+ *
+ * This method consumes all of [paddingValues] in addition to whatever has been
+ * consumed by other [windowInsetsPadding] modifiers by ancestors. [consumedWindowInsets]
+ * accepting a [WindowInsets] argument ensures that its insets are consumed and doesn't
+ * consume more if they have already been consumed by ancestors.
+ *
+ * @sample androidx.compose.foundation.layout.samples.consumedInsetsPaddingSample
+ */
+@ExperimentalLayoutApi
+@Stable
+fun Modifier.consumedWindowInsets(paddingValues: PaddingValues): Modifier = this.then(
+    PaddingValuesConsumingModifier(paddingValues, debugInspectorInfo {
+        name = "consumedWindowInsets"
+        properties["paddingValues"] = paddingValues
+    })
+)
+
+internal val ModifierLocalConsumedWindowInsets = modifierLocalOf {
+    WindowInsets(0, 0, 0, 0)
+}
+
+internal class InsetsPaddingModifier(
+    private val insets: WindowInsets,
+    inspectorInfo: InspectorInfo.() -> Unit = debugInspectorInfo {
+        name = "InsetsPaddingModifier"
+        properties["insets"] = insets
+    }
+) : InspectorValueInfo(inspectorInfo), LayoutModifier,
+    ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
+    private var unconsumedInsets: WindowInsets by mutableStateOf(insets)
+    private var consumedInsets: WindowInsets by mutableStateOf(insets)
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val left = unconsumedInsets.getLeft(this, layoutDirection)
+        val top = unconsumedInsets.getTop(this)
+        val right = unconsumedInsets.getRight(this, layoutDirection)
+        val bottom = unconsumedInsets.getBottom(this)
+
+        val horizontal = left + right
+        val vertical = top + bottom
+
+        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+
+        val width = constraints.constrainWidth(placeable.width + horizontal)
+        val height = constraints.constrainHeight(placeable.height + vertical)
+        return layout(width, height) {
+            placeable.place(left, top)
+        }
+    }
+
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+        with(scope) {
+            val consumed = ModifierLocalConsumedWindowInsets.current
+            unconsumedInsets = insets.exclude(consumed)
+            consumedInsets = consumed.union(insets)
+        }
+    }
+
+    override val key: ProvidableModifierLocal<WindowInsets>
+        get() = ModifierLocalConsumedWindowInsets
+
+    override val value: WindowInsets
+        get() = consumedInsets
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is InsetsPaddingModifier) {
+            return false
+        }
+
+        return other.insets == insets
+    }
+
+    override fun hashCode(): Int = insets.hashCode()
+}
+
+/**
+ * Base class for arbitrary insets consumption modifiers.
+ */
+@Stable
+private sealed class InsetsConsumingModifier(
+    inspectorInfo: InspectorInfo.() -> Unit
+) : InspectorValueInfo(inspectorInfo), ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
+    private var consumedInsets: WindowInsets by mutableStateOf(WindowInsets(0, 0, 0, 0))
+
+    abstract fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets
+
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+        with(scope) {
+            val current = ModifierLocalConsumedWindowInsets.current
+            consumedInsets = calculateInsets(current)
+        }
+    }
+
+    override val key: ProvidableModifierLocal<WindowInsets>
+        get() = ModifierLocalConsumedWindowInsets
+
+    override val value: WindowInsets
+        get() = consumedInsets
+}
+
+@Stable
+private class PaddingValuesConsumingModifier(
+    private val paddingValues: PaddingValues,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : InsetsConsumingModifier(inspectorInfo) {
+    override fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets =
+        paddingValues.asInsets().add(modifierLocalInsets)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is PaddingValuesConsumingModifier) {
+            return false
+        }
+
+        return other.paddingValues == paddingValues
+    }
+
+    override fun hashCode(): Int = paddingValues.hashCode()
+}
+
+@Stable
+private class UnionInsetsConsumingModifier(
+    private val insets: WindowInsets,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : InsetsConsumingModifier(inspectorInfo) {
+    override fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets =
+        insets.union(modifierLocalInsets)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is UnionInsetsConsumingModifier) {
+            return false
+        }
+
+        return other.insets == insets
+    }
+
+    override fun hashCode(): Int = insets.hashCode()
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
new file mode 100644
index 0000000..59cd2a6
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2022 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.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Sets the width to that of [insets] at the [start][androidx.compose.ui.Alignment.Start]
+ * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
+ * depending on the [LayoutDirection].
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsStartWidthSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsStartWidth(insets: WindowInsets) = this.then(
+    DerivedWidthModifier(insets, debugInspectorInfo {
+        name = "insetsStartWidth"
+        properties["insets"] = insets
+    }) { layoutDirection, density ->
+        if (layoutDirection == LayoutDirection.Ltr) {
+            getLeft(density, layoutDirection)
+        } else {
+            getRight(density, layoutDirection)
+        }
+    }
+)
+
+/**
+ * Sets the width to that of [insets] at the [end][androidx.compose.ui.Alignment.End]
+ * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
+ * depending on the [LayoutDirection].
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsEndWidthSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsEndWidth(insets: WindowInsets) = this.then(
+    DerivedWidthModifier(insets, debugInspectorInfo {
+        name = "insetsEndWidth"
+        properties["insets"] = insets
+    }) { layoutDirection, density ->
+        if (layoutDirection == LayoutDirection.Rtl) {
+            getLeft(density, layoutDirection)
+        } else {
+            getRight(density, layoutDirection)
+        }
+    }
+)
+
+/**
+ * Sets the height to that of [insets] at the [top][WindowInsets.getTop] of the screen.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsTopHeightSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsTopHeight(insets: WindowInsets) = this.then(
+    DerivedHeightModifier(insets, debugInspectorInfo {
+        name = "insetsTopHeight"
+        properties["insets"] = insets
+    }) {
+        getTop(it)
+    }
+)
+
+/**
+ * Sets the height to that of [insets] at the [bottom][WindowInsets.getBottom] of the screen.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsBottomHeightSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsBottomHeight(insets: WindowInsets) = this.then(
+    DerivedHeightModifier(insets, debugInspectorInfo {
+        name = "insetsBottomHeight"
+        properties["insets"] = insets
+    }) {
+        getBottom(it)
+    }
+)
+
+/**
+ * Sets the width based on [widthCalc]. If the width is 0, the height will also always be 0
+ * and the content will not be placed.
+ */
+@Stable
+private class DerivedWidthModifier(
+    private val insets: WindowInsets,
+    inspectorInfo: InspectorInfo.() -> Unit,
+    private val widthCalc: WindowInsets.(LayoutDirection, Density) -> Int
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val width = insets.widthCalc(layoutDirection, this)
+        if (width == 0) {
+            return layout(0, 0) { }
+        }
+        // check for height first
+        val childConstraints = constraints.copy(minWidth = width, maxWidth = width)
+        val placeable = measurable.measure(childConstraints)
+        return layout(width, placeable.height) {
+            placeable.placeRelative(0, 0)
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is DerivedWidthModifier) {
+            return false
+        }
+        return insets == other.insets && widthCalc == other.widthCalc
+    }
+
+    override fun hashCode(): Int = 31 * insets.hashCode() + widthCalc.hashCode()
+}
+
+/**
+ * Sets the height based on [heightCalc]. If the height is 0, the width will also always be 0
+ * and the content will not be placed.
+ */
+@Stable
+private class DerivedHeightModifier(
+    private val insets: WindowInsets,
+    inspectorInfo: InspectorInfo.() -> Unit,
+    private val heightCalc: WindowInsets.(Density) -> Int
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val height = insets.heightCalc(this)
+        if (height == 0) {
+            return layout(0, 0) { }
+        }
+        // check for height first
+        val childConstraints = constraints.copy(minHeight = height, maxHeight = height)
+        val placeable = measurable.measure(childConstraints)
+        return layout(placeable.width, height) {
+            placeable.placeRelative(0, 0)
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other !is DerivedHeightModifier) {
+            return false
+        }
+        return insets == other.insets && heightCalc == other.heightCalc
+    }
+
+    override fun hashCode(): Int = 31 * insets.hashCode() + heightCalc.hashCode()
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
index cb891d7..308b085 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
@@ -30,7 +30,6 @@
 import androidx.compose.testutils.LayeredComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalTextInputService
 import androidx.compose.ui.text.benchmark.RandomTextGenerator
@@ -101,6 +100,5 @@
         override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
             /*do nothing*/
         }
-        override fun notifyFocusedRect(rect: Rect) { /*do nothing*/ }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 9526465..f82b997 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -50,6 +50,7 @@
                 ComposableDemo("Inside Dialog") { onNavigateUp ->
                     DialogInputFieldDemo(onNavigateUp)
                 },
+                ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() }
             )
         ),
         ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt
new file mode 100644
index 0000000..20deaab
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 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.demos.text
+
+import android.app.Activity
+import android.content.ContextWrapper
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+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.platform.LocalContext
+import androidx.compose.ui.unit.dp
+
+private enum class ScrollableType {
+    ScrollableColumn,
+    LazyColumn
+}
+
+@Composable
+fun TextFieldsInScrollableDemo() {
+    var adjustResize by remember { mutableStateOf(false) }
+    var scrollableType by remember { mutableStateOf(ScrollableType.values().first()) }
+
+    @Suppress("DEPRECATION")
+    SoftInputMode(if (adjustResize) SOFT_INPUT_ADJUST_RESIZE else SOFT_INPUT_ADJUST_PAN)
+
+    Column {
+        Row(verticalAlignment = Alignment.CenterVertically) {
+            Text("ADJUST_PAN")
+            Switch(adjustResize, onCheckedChange = { adjustResize = it })
+            Text("ADJUST_RESIZE")
+        }
+        Row(verticalAlignment = Alignment.CenterVertically) {
+            Text("Scrollable column")
+            Switch(
+                checked = scrollableType == ScrollableType.LazyColumn,
+                onCheckedChange = {
+                    scrollableType = if (it) {
+                        ScrollableType.LazyColumn
+                    } else {
+                        ScrollableType.ScrollableColumn
+                    }
+                })
+            Text("LazyColumn")
+        }
+
+        when (scrollableType) {
+            ScrollableType.ScrollableColumn -> TextFieldInScrollableColumn()
+            ScrollableType.LazyColumn -> TextFieldInLazyColumn()
+        }
+    }
+}
+
+@Composable
+fun TextFieldInScrollableColumn() {
+    Column(
+        Modifier.verticalScroll(rememberScrollState())
+    ) {
+        repeat(100) { index ->
+            DemoTextField(index)
+        }
+    }
+}
+
+@Composable
+fun TextFieldInLazyColumn() {
+    LazyColumn {
+        items(100) { index ->
+            DemoTextField(index)
+        }
+    }
+}
+
+@Composable
+private fun DemoTextField(index: Int) {
+    var text by rememberSaveable { mutableStateOf("") }
+    TextField(
+        value = text,
+        onValueChange = { text = it },
+        leadingIcon = { Text(index.toString()) },
+        modifier = Modifier
+            .padding(4.dp)
+            .border(1.dp, Color.Black)
+            .fillMaxWidth()
+    )
+}
+
+/**
+ * Sets the window's [softInputMode][android.view.Window.setSoftInputMode] to [mode] as long as this
+ * function is composed.
+ */
+@Composable
+private fun SoftInputMode(mode: Int) {
+    val context = LocalContext.current
+    DisposableEffect(context, mode) {
+        val activity = generateSequence(context) { (context as? ContextWrapper)?.baseContext }
+            .filterIsInstance<Activity>()
+            .firstOrNull()
+            ?: return@DisposableEffect onDispose {}
+        val originalMode = activity.window.attributes.softInputMode
+        activity.window.setSoftInputMode(mode)
+        onDispose {
+            activity.window.setSoftInputMode(originalMode)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
new file mode 100644
index 0000000..421bdef
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.ScrollableType.LazyList
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.ScrollableType.ScrollableColumn
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.SoftInputMode.AdjustPan
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.SoftInputMode.AdjustResize
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.isFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+@RunWith(Parameterized::class)
+class CoreTextFieldKeyboardScrollableInteractionTest(
+    private val scrollableType: ScrollableType,
+    private val softInputMode: SoftInputMode,
+    private val withDecorationPadding: Boolean,
+) {
+    enum class ScrollableType {
+        ScrollableColumn,
+        LazyList
+    }
+
+    enum class SoftInputMode {
+        AdjustResize,
+        AdjustPan
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "scrollableType={0}, softInputMode={1}, withDecorationPadding={2}")
+        fun parameters(): Iterable<Array<*>> = crossProductOf(
+            ScrollableType.values(),
+            SoftInputMode.values(),
+            arrayOf(false, true), // withDecorationPadding
+        )
+    }
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val ListTag = "list"
+    private val keyboardHelper = KeyboardHelper(rule)
+
+    @Test
+    fun test() {
+        // TODO(b/192043120) This is known broken when soft input mode is Resize.
+        assumeTrue(softInputMode != AdjustResize)
+
+        rule.setContent {
+            keyboardHelper.view = LocalView.current
+            TestContent()
+        }
+
+        // This test is all about the keyboard going from hidden to shown, so hide it to start.
+        keyboardHelper.hideKeyboardIfShown()
+
+        rule.onNodeWithTag(ListTag)
+            .performTouchInput {
+                // Click one pixel above the bottom of the list.
+                click(bottomCenter - Offset(0f, 1f))
+            }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
+
+        rule.onNode(isFocused()).assertIsDisplayed()
+    }
+
+    @Composable
+    fun TestContent() {
+        @Suppress("DEPRECATION")
+        SoftInputMode(
+            when (softInputMode) {
+                AdjustResize -> SOFT_INPUT_ADJUST_RESIZE
+                AdjustPan -> SOFT_INPUT_ADJUST_PAN
+            }
+        )
+
+        val itemCount = 100
+        when (scrollableType) {
+            ScrollableColumn -> {
+                Column(
+                    Modifier
+                        .testTag(ListTag)
+                        .verticalScroll(rememberScrollState())
+                ) {
+                    repeat(itemCount) { index ->
+                        TestTextField(index)
+                    }
+                }
+            }
+            LazyList -> {
+                LazyColumn(Modifier.testTag(ListTag)) {
+                    items(itemCount) { index ->
+                        TestTextField(index)
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun TestTextField(index: Int) {
+        var isFocused by remember { mutableStateOf(false) }
+        CoreTextField(
+            value = TextFieldValue(text = index.toString()),
+            onValueChange = {},
+            modifier = Modifier
+                .fillMaxWidth()
+                .drawWithContent {
+                    drawContent()
+                    if (isFocused) {
+                        drawRect(Color.Blue, style = Stroke(2.dp.toPx()))
+                    }
+                }
+                .onFocusChanged { isFocused = it.isFocused }
+                .testTag(index.toString()),
+            decorationBox = { inner ->
+                if (withDecorationPadding) {
+                    Box(Modifier.padding(vertical = 24.dp)) {
+                        inner()
+                    }
+                } else {
+                    inner()
+                }
+            }
+        )
+    }
+
+    @Composable
+    private fun SoftInputMode(mode: Int) {
+        val context = LocalContext.current
+        DisposableEffect(mode) {
+            val activity = context.findActivityOrNull() ?: return@DisposableEffect onDispose {}
+            val originalMode = activity.window.attributes.softInputMode
+            activity.window.setSoftInputMode(mode)
+            onDispose {
+                activity.window.setSoftInputMode(originalMode)
+            }
+        }
+    }
+
+    private tailrec fun Context.findActivityOrNull(): Activity? {
+        return (this as? Activity)
+            ?: (this as? ContextWrapper)?.baseContext?.findActivityOrNull()
+    }
+}
+
+private fun crossProductOf(vararg values: Array<*>): List<Array<*>> =
+    crossProductOf(values.map { it.asSequence() })
+        .map { it.toList().toTypedArray() }
+        .toList()
+
+private fun crossProductOf(values: List<Sequence<*>>): Sequence<Sequence<*>> =
+    when (values.size) {
+        0 -> emptySequence()
+        1 -> values[0].map { sequenceOf(it) }
+        else -> sequence {
+            for (subProduct in crossProductOf(values.subList(1, values.size)))
+                for (firstValue in values[0]) {
+                    yield(sequenceOf(firstValue) + subProduct)
+                }
+        }
+    }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
index ae3ed31..6cdc2c7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -17,10 +17,6 @@
 package androidx.compose.foundation.text
 
 import android.os.Build
-import android.view.View
-import android.view.WindowInsets
-import android.view.WindowInsetsAnimation
-import androidx.annotation.RequiresApi
 import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -39,12 +35,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -52,9 +45,9 @@
     @get:Rule
     val rule = createComposeRule()
 
-    private lateinit var view: View
     private lateinit var focusManager: FocusManager
     private val timeout = 15_000L
+    private val keyboardHelper = KeyboardHelper(rule, timeout)
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
     @Test
@@ -72,7 +65,7 @@
         rule.onNodeWithTag("TextField1").performClick()
 
         // Assert.
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -92,7 +85,7 @@
         rule.runOnIdle { focusRequester.requestFocus() }
 
         // Assert.
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -109,13 +102,13 @@
         }
         // Request focus and wait for keyboard.
         rule.runOnIdle { focusRequester.requestFocus() }
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
 
         // Act.
         rule.runOnIdle { focusManager.clearFocus() }
 
         // Assert.
-        view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = false)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -123,7 +116,6 @@
     fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
         // Arrange.
         rule.setContentForTest {
-            view = LocalView.current
             CoreTextField(
                 value = TextFieldValue("Hello"),
                 onValueChange = {},
@@ -131,15 +123,15 @@
             )
         }
         rule.onNodeWithTag("TextField1").performClick()
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
 
         // Act.
-        rule.runOnIdle { view.hideKeyboard() }
-        view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+        rule.runOnIdle { keyboardHelper.hideKeyboard() }
+        keyboardHelper.waitForKeyboardVisibility(visible = false)
         rule.onNodeWithTag("TextField1").performClick()
 
         // Assert.
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
     }
 
     @OptIn(ExperimentalComposeUiApi::class)
@@ -163,64 +155,23 @@
             }
         }
         rule.runOnIdle { focusRequester1.requestFocus() }
-        view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
 
         // Act.
         rule.runOnIdle { focusRequester2.requestFocus() }
 
         // Assert.
-        view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+        keyboardHelper.waitForKeyboardVisibility(visible = false)
     }
 
     private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
         setContent {
-            view = LocalView.current
+            keyboardHelper.view = LocalView.current
             focusManager = LocalFocusManager.current
             composable()
         }
         // We experienced some flakiness in tests if the keyboard was visible at the start of the
         // test. So we make sure that the keyboard is hidden at the start of every test.
-        runOnIdle {
-            if (view.isSoftwareKeyboardShown()) {
-                view.hideKeyboard()
-                view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
-            }
-        }
+        keyboardHelper.hideKeyboardIfShown()
     }
 }
-
-private fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
-    val latch = CountDownLatch(1)
-    rootView.setWindowInsetsAnimationCallback(
-        InsetAnimationCallback {
-            if (condition()) { latch.countDown() }
-        }
-    )
-    val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
-    assertThat(conditionMet).isTrue()
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private class InsetAnimationCallback(val block: () -> Unit) :
-    WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
-
-    override fun onProgress(
-        insets: WindowInsets,
-        runningAnimations: MutableList<WindowInsetsAnimation>
-    ) = insets
-
-    override fun onEnd(animation: WindowInsetsAnimation) {
-        block()
-        super.onEnd(animation)
-    }
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private fun View.isSoftwareKeyboardShown(): Boolean {
-    return rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsets.Type.ime())
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private fun View.hideKeyboard() {
-    windowInsetsController?.hide(WindowInsets.Type.ime())
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
new file mode 100644
index 0000000..87fe0e8
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.content.Context
+import android.os.Build
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Helper methods for hiding and showing the keyboard in tests.
+ * Must set [view] before calling any methods on this class.
+ */
+class KeyboardHelper(
+    private val composeRule: ComposeTestRule,
+    private val timeout: Long = 15_000L
+) {
+    /**
+     * The [View] hosting the compose rule's content. Must be set before calling any methods on this
+     * class.
+     */
+    lateinit var view: View
+    private val imm by lazy {
+        view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+    }
+
+    /**
+     * Requests the keyboard to be hidden without waiting for it.
+     * Should be called from the main thread.
+     */
+    fun hideKeyboard() {
+        if (Build.VERSION.SDK_INT >= 30) {
+            hideKeyboardWithInsets()
+        } else {
+            hideKeyboardWithImm()
+        }
+    }
+
+    /**
+     * Blocks until the [timeout] or the keyboard's visibility matches [visible].
+     * May be called from the test thread or the main thread.
+     */
+    fun waitForKeyboardVisibility(visible: Boolean) {
+        waitUntil(timeout) {
+            isSoftwareKeyboardShown() == visible
+        }
+    }
+
+    fun hideKeyboardIfShown() {
+        composeRule.runOnIdle {
+            if (isSoftwareKeyboardShown()) {
+                hideKeyboard()
+                waitForKeyboardVisibility(visible = false)
+            }
+        }
+    }
+
+    private fun isSoftwareKeyboardShown(): Boolean {
+        return if (Build.VERSION.SDK_INT >= 30) {
+            isSoftwareKeyboardShownWithInsets()
+        } else {
+            isSoftwareKeyboardShownWithImm()
+        }
+    }
+
+    @RequiresApi(30)
+    private fun isSoftwareKeyboardShownWithInsets(): Boolean {
+        return view.rootWindowInsets != null &&
+            view.rootWindowInsets.isVisible(WindowInsets.Type.ime())
+    }
+
+    private fun isSoftwareKeyboardShownWithImm(): Boolean {
+        // TODO(b/163742556): This is just a proxy for software keyboard visibility. Find a better
+        //  way to check if the software keyboard is shown.
+        return imm.isAcceptingText
+    }
+
+    private fun hideKeyboardWithImm() {
+        imm.hideSoftInputFromWindow(view.windowToken, 0)
+    }
+
+    @RequiresApi(30)
+    private fun hideKeyboardWithInsets() {
+        view.windowInsetsController?.hide(WindowInsets.Type.ime())
+    }
+
+    private fun waitUntil(timeout: Long, condition: () -> Boolean) {
+        if (Build.VERSION.SDK_INT >= 30) {
+            view.waitUntil(timeout, condition)
+        } else {
+            composeRule.waitUntil(timeout, condition)
+        }
+    }
+}
+
+@RequiresApi(30)
+fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
+    val latch = CountDownLatch(1)
+    rootView.setWindowInsetsAnimationCallback(
+        InsetAnimationCallback {
+            if (condition()) {
+                latch.countDown()
+            }
+        }
+    )
+    val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
+    assertThat(conditionMet).isTrue()
+}
+
+@RequiresApi(30)
+private class InsetAnimationCallback(val block: () -> Unit) :
+    WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+
+    override fun onProgress(
+        insets: WindowInsets,
+        runningAnimations: MutableList<WindowInsetsAnimation>
+    ) = insets
+
+    override fun onEnd(animation: WindowInsetsAnimation) {
+        block()
+        super.onEnd(animation)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 67e42d6..ff69b33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -62,6 +62,17 @@
     val scope = rememberCoroutineScope()
     val focusedInteraction = remember { mutableStateOf<FocusInteraction.Focus?>(null) }
     var isFocused by remember { mutableStateOf(false) }
+
+    // Focusables have a few different cases where they need to make sure they stay visible:
+    //
+    // 1. Focusable node newly receives focus – always bring entire node into view. That's what this
+    //    BringIntoViewRequester does.
+    // 2. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
+    //    into view if it was also in view before the resize. This handles the case of
+    //    `softInputMode=ADJUST_RESIZE`. See b/216842427.
+    // 3. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused
+    //    rect to the view system, and the view system itself will keep the focused area in view.
+    //    See aosp/1964580.
     @OptIn(ExperimentalFoundationApi::class)
     val bringIntoViewRequester = remember { BringIntoViewRequester() }
     DisposableEffect(interactionSource) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 348fa20..aabc044 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -16,10 +16,13 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
 import androidx.compose.foundation.text.selection.SelectionHandleInfo
 import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
@@ -35,6 +38,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
@@ -42,6 +46,7 @@
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Paint
@@ -100,6 +105,7 @@
 import androidx.compose.ui.unit.Density
 import kotlin.math.max
 import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
 
 /**
  * Base composable that enables users to edit text via hardware or software keyboard.
@@ -163,7 +169,7 @@
  * innerTextField exactly once.
  */
 @Composable
-@OptIn(InternalFoundationTextApi::class)
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
 internal fun CoreTextField(
     value: TextFieldValue,
     onValueChange: (TextFieldValue) -> Unit,
@@ -256,6 +262,9 @@
     manager.focusRequester = focusRequester
     manager.editable = !readOnly
 
+    val coroutineScope = rememberCoroutineScope()
+    val bringIntoViewRequester = remember { BringIntoViewRequester() }
+
     // Focus
     val focusModifier = Modifier.textFieldFocusModifier(
         enabled = enabled,
@@ -272,9 +281,28 @@
                 textInputService,
                 state,
                 value,
-                imeOptions,
-                offsetMapping
+                imeOptions
             )
+
+            // The focusable modifier itself will request the entire focusable be brought into view
+            // when it gains focus – in this case, that's the decoration box. However, since text
+            // fields may have their own internal scrolling, and the decoration box can do anything,
+            // we also need to specifically request that the cursor itself be brought into view.
+            // TODO(b/216790855) If this request happens after the focusable's request, the field
+            //  will only be scrolled far enough to show the cursor, _not_ the entire decoration
+            //  box.
+            if (it.isFocused) {
+                state.layoutResult?.let { layoutResult ->
+                    coroutineScope.launch {
+                        bringIntoViewRequester.bringSelectionEndIntoView(
+                            value,
+                            state.textDelegate,
+                            layoutResult.value,
+                            offsetMapping
+                        )
+                    }
+                }
+            }
         }
         if (!it.isFocused) manager.deselect()
     }
@@ -342,19 +370,6 @@
                 state.showCursorHandle =
                     manager.isSelectionHandleInVisibleBound(isStartHandle = true)
             }
-            state.layoutResult?.let { layoutResult ->
-                state.inputSession?.let { inputSession ->
-                    TextFieldDelegate.notifyFocusedRect(
-                        value,
-                        state.textDelegate,
-                        layoutResult.value,
-                        it,
-                        inputSession,
-                        state.hasFocus,
-                        offsetMapping
-                    )
-                }
-            }
         }
         state.layoutResult?.innerTextFieldCoordinates = it
     }
@@ -520,6 +535,7 @@
                 .textFieldMinSize(textStyle)
                 .then(onPositionedModifier)
                 .then(magnifierModifier)
+                .bringIntoViewRequester(bringIntoViewRequester)
 
             SimpleLayout(coreTextFieldModifier) {
                 Layout(
@@ -789,13 +805,12 @@
     }
 }
 
-@OptIn(InternalFoundationTextApi::class)
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
 private fun notifyTextInputServiceOnFocusChange(
     textInputService: TextInputService,
     state: TextFieldState,
     value: TextFieldValue,
-    imeOptions: ImeOptions,
-    offsetMapping: OffsetMapping
+    imeOptions: ImeOptions
 ) {
     if (state.hasFocus) {
         state.inputSession = TextFieldDelegate.onFocus(
@@ -805,21 +820,7 @@
             imeOptions,
             state.onValueChange,
             state.onImeActionPerformed
-        ).also { newSession ->
-            state.layoutCoordinates?.let { coords ->
-                state.layoutResult?.let { layoutResult ->
-                    TextFieldDelegate.notifyFocusedRect(
-                        value,
-                        state.textDelegate,
-                        layoutResult.value,
-                        coords,
-                        newSession,
-                        state.hasFocus,
-                        offsetMapping
-                    )
-                }
-            }
-        }
+        )
     } else {
         state.inputSession?.let { session ->
             TextFieldDelegate.onBlur(session, state.processor, state.onValueChange)
@@ -828,6 +829,54 @@
     }
 }
 
+/**
+ * Calculates the location of the end of the current selection and requests that it be brought into
+ * view using [bringIntoView][BringIntoViewRequester.bringIntoView].
+ *
+ * Text fields have a lot of different edge cases where they need to make sure they stay visible:
+ *
+ * 1. Focusable node newly receives focus – always bring entire node into view.
+ * 2. Unfocused text field is tapped – always bring cursor area into view (conflicts with above, see
+ *    b/216790855).
+ * 3. Focused text field is tapped – always bring cursor area into view.
+ * 4. Text input occurs – always bring cursor area into view.
+ * 5. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
+ *    into view if it was also in view before the resize. This handles the case of
+ *    `softInputMode=ADJUST_RESIZE`. See b/216842427.
+ * 6. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused rect to
+ *    the view system, and the view system itself will keep the focused area in view.
+ *    See aosp/1964580.
+ *
+ * This function is used to handle 2, 3, and 4, and the others are automatically handled by the
+ * focus system.
+ */
+@OptIn(ExperimentalFoundationApi::class, InternalFoundationTextApi::class)
+internal suspend fun BringIntoViewRequester.bringSelectionEndIntoView(
+    value: TextFieldValue,
+    textDelegate: TextDelegate,
+    textLayoutResult: TextLayoutResult,
+    offsetMapping: OffsetMapping
+) {
+    val selectionEndInTransformed = offsetMapping.originalToTransformed(value.selection.max)
+    val selectionEndBounds = when {
+        selectionEndInTransformed < textLayoutResult.layoutInput.text.length -> {
+            textLayoutResult.getBoundingBox(selectionEndInTransformed)
+        }
+        selectionEndInTransformed != 0 -> {
+            textLayoutResult.getBoundingBox(selectionEndInTransformed - 1)
+        }
+        else -> { // empty text.
+            val defaultSize = computeSizeForDefaultText(
+                textDelegate.style,
+                textDelegate.density,
+                textDelegate.fontFamilyResolver
+            )
+            Rect(0f, 0f, 1.0f, defaultSize.height.toFloat())
+        }
+    }
+    bringIntoView(selectionEndBounds)
+}
+
 @Composable
 private fun SelectionToolbarAndHandles(manager: TextFieldSelectionManager, show: Boolean) {
     if (show) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
index 2f001a7..c892fcc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
@@ -17,11 +17,8 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.Paragraph
 import androidx.compose.ui.text.SpanStyle
@@ -134,55 +131,6 @@
         }
 
         /**
-         * Notify system that focused input area.
-         *
-         * System is typically scrolled up not to be covered by keyboard.
-         *
-         * @param value The editor model
-         * @param textDelegate The text delegate
-         * @param layoutCoordinates The layout coordinates
-         * @param textInputSession The current input session.
-         * @param hasFocus True if focus is gained.
-         * @param offsetMapping The mapper from/to editing buffer to/from visible text.
-         */
-        @JvmStatic
-        internal fun notifyFocusedRect(
-            value: TextFieldValue,
-            textDelegate: TextDelegate,
-            textLayoutResult: TextLayoutResult,
-            layoutCoordinates: LayoutCoordinates,
-            textInputSession: TextInputSession,
-            hasFocus: Boolean,
-            offsetMapping: OffsetMapping
-        ) {
-            if (!hasFocus) {
-                return
-            }
-            val focusOffsetInTransformed = offsetMapping.originalToTransformed(value.selection.max)
-            val bbox = when {
-                focusOffsetInTransformed < textLayoutResult.layoutInput.text.length -> {
-                    textLayoutResult.getBoundingBox(focusOffsetInTransformed)
-                }
-                focusOffsetInTransformed != 0 -> {
-                    textLayoutResult.getBoundingBox(focusOffsetInTransformed - 1)
-                }
-                else -> { // empty text.
-                    val defaultSize = computeSizeForDefaultText(
-                        textDelegate.style,
-                        textDelegate.density,
-                        textDelegate.fontFamilyResolver
-                    )
-                    Rect(0f, 0f, 1.0f, defaultSize.height.toFloat())
-                }
-            }
-            val globalLT = layoutCoordinates.localToRoot(Offset(bbox.left, bbox.top))
-
-            textInputSession.notifyFocusedRect(
-                Rect(Offset(globalLT.x, globalLT.y), Size(bbox.width, bbox.height))
-            )
-        }
-
-        /**
          * Called when edit operations are passed from TextInputService
          *
          * @param ops A list of edit operations.
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
new file mode 100644
index 0000000..9296223
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.reset
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.verifyBlocking
+import com.nhaarman.mockitokotlin2.whenever
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class TextFieldBringIntoViewTest {
+
+    private val delegate: TextDelegate = mock()
+    private var layoutCoordinates: LayoutCoordinates = mock()
+    private val textLayoutResultProxy: TextLayoutResultProxy = mock()
+    private val textLayoutResult: TextLayoutResult = mock()
+    private val bringIntoViewRequester: BringIntoViewRequester = mock()
+
+    /**
+     * Test implementation of offset map which doubles the offset in transformed text.
+     */
+    private val skippingOffsetMap = object : OffsetMapping {
+        override fun originalToTransformed(offset: Int): Int = offset * 2
+        override fun transformedToOriginal(offset: Int): Int = offset / 2
+    }
+
+    @Before
+    fun setup() {
+        whenever(textLayoutResultProxy.value).thenReturn(textLayoutResult)
+    }
+
+    @Test
+    fun notify_focused_rect() {
+        val rect = Rect(0f, 1f, 2f, 3f)
+        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+        val point = Offset(5f, 6f)
+        layoutCoordinates = MockCoordinates(
+            rootOffset = point
+        )
+        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
+
+        val input = TextLayoutInput(
+            text = AnnotatedString(editorState.text),
+            style = TextStyle(),
+            placeholders = listOf(),
+            maxLines = Int.MAX_VALUE,
+            softWrap = true,
+            overflow = TextOverflow.Clip,
+            density = Density(1.0f),
+            layoutDirection = LayoutDirection.Ltr,
+            fontFamilyResolver = mock(),
+            constraints = mock()
+        )
+        whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                editorState,
+                delegate,
+                textLayoutResult,
+                OffsetMapping.Identity
+            )
+        }
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+    }
+
+    @Test
+    fun notify_rect_tail() {
+        val rect = Rect(0f, 1f, 2f, 3f)
+        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+        val point = Offset(5f, 6f)
+        layoutCoordinates = MockCoordinates(
+            rootOffset = point
+        )
+        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(12))
+        val input = TextLayoutInput(
+            text = AnnotatedString(editorState.text),
+            style = TextStyle(),
+            placeholders = listOf(),
+            maxLines = Int.MAX_VALUE,
+            softWrap = true,
+            overflow = TextOverflow.Clip,
+            density = Density(1.0f),
+            layoutDirection = LayoutDirection.Ltr,
+            fontFamilyResolver = mock(),
+            constraints = mock()
+        )
+        whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                editorState,
+                delegate,
+                textLayoutResult,
+                OffsetMapping.Identity
+            )
+        }
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+    }
+
+    @Test
+    fun check_notify_rect_uses_offset_map() {
+        val rect = Rect(0f, 1f, 2f, 3f)
+        val point = Offset(5f, 6f)
+        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1, 3))
+
+        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+        val input = TextLayoutInput(
+            text = AnnotatedString(editorState.text),
+            style = TextStyle(),
+            placeholders = listOf(),
+            maxLines = Int.MAX_VALUE,
+            softWrap = true,
+            overflow = TextOverflow.Clip,
+            density = Density(1.0f),
+            layoutDirection = LayoutDirection.Ltr,
+            fontFamilyResolver = mock(),
+            constraints = mock()
+        )
+        whenever(textLayoutResult.layoutInput).thenReturn(input)
+        layoutCoordinates = MockCoordinates(
+            rootOffset = point
+        )
+
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                editorState,
+                delegate,
+                textLayoutResult,
+                skippingOffsetMap
+            )
+        }
+        verify(textLayoutResult).getBoundingBox(6)
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+    }
+
+    @Test
+    fun notify_transformed_text() {
+        val rect = Rect(0f, 1f, 2f, 3f)
+        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+        val point = Offset(5f, 6f)
+        layoutCoordinates = MockCoordinates(
+            rootOffset = point
+        )
+
+        val input = TextLayoutInput(
+            // In this test case, transform the text into double characters text.
+            text = AnnotatedString("HHeelllloo,,  WWoorrlldd"),
+            style = TextStyle(),
+            placeholders = listOf(),
+            maxLines = Int.MAX_VALUE,
+            softWrap = true,
+            overflow = TextOverflow.Clip,
+            density = Density(1.0f),
+            layoutDirection = LayoutDirection.Ltr,
+            fontFamilyResolver = mock(),
+            constraints = mock()
+        )
+        whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+        val offsetMapping = object : OffsetMapping {
+            override fun originalToTransformed(offset: Int): Int = offset * 2
+            override fun transformedToOriginal(offset: Int): Int = offset / 2
+        }
+
+        // The beginning of the text.
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                TextFieldValue(text = "Hello, World", selection = TextRange(0)),
+                delegate,
+                textLayoutResult,
+                offsetMapping
+            )
+        }
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+
+        // The tail of the transformed text.
+        reset(bringIntoViewRequester)
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                TextFieldValue(text = "Hello, World", selection = TextRange(24)),
+                delegate,
+                textLayoutResult,
+                offsetMapping
+            )
+        }
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+
+        // Beyond the tail of the transformed text.
+        reset(bringIntoViewRequester)
+        runBlocking {
+            bringIntoViewRequester.bringSelectionEndIntoView(
+                TextFieldValue(text = "Hello, World", selection = TextRange(25)),
+                delegate,
+                textLayoutResult,
+                offsetMapping
+            )
+        }
+        verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+    }
+
+    private class MockCoordinates(
+        override val size: IntSize = IntSize.Zero,
+        val localOffset: Offset = Offset.Zero,
+        val globalOffset: Offset = Offset.Zero,
+        val rootOffset: Offset = Offset.Zero
+    ) : LayoutCoordinates {
+        override val providedAlignmentLines: Set<AlignmentLine>
+            get() = emptySet()
+        override val parentLayoutCoordinates: LayoutCoordinates?
+            get() = null
+        override val parentCoordinates: LayoutCoordinates?
+            get() = null
+        override val isAttached: Boolean
+            get() = true
+
+        override fun windowToLocal(relativeToWindow: Offset): Offset = localOffset
+
+        override fun localToWindow(relativeToLocal: Offset): Offset = globalOffset
+
+        override fun localToRoot(relativeToLocal: Offset): Offset = rootOffset
+        override fun localPositionOf(
+            sourceCoordinates: LayoutCoordinates,
+            relativeToSource: Offset
+        ): Offset = Offset.Zero
+
+        override fun localBoundingBoxOf(
+            sourceCoordinates: LayoutCoordinates,
+            clipBounds: Boolean
+        ): Rect = Rect.Zero
+
+        override fun get(alignmentLine: AlignmentLine): Int = 0
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
index 6ec1a8a..d586f8f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
@@ -17,18 +17,14 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.MultiParagraphIntrinsics
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextLayoutInput
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.EditProcessor
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
@@ -41,17 +37,11 @@
 import androidx.compose.ui.text.input.TransformedText
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.eq
 import com.nhaarman.mockitokotlin2.inOrder
 import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
-import com.nhaarman.mockitokotlin2.reset
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import com.nhaarman.mockitokotlin2.whenever
@@ -184,133 +174,6 @@
     }
 
     @Test
-    fun notify_focused_rect() {
-        val rect = Rect(0f, 1f, 2f, 3f)
-        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
-        val point = Offset(5f, 6f)
-        layoutCoordinates = MockCoordinates(
-            rootOffset = point
-        )
-        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
-        val textInputSession: TextInputSession = mock()
-
-        val input = TextLayoutInput(
-            text = AnnotatedString(editorState.text),
-            style = TextStyle(),
-            placeholders = listOf(),
-            maxLines = Int.MAX_VALUE,
-            softWrap = true,
-            overflow = TextOverflow.Clip,
-            density = Density(1.0f),
-            layoutDirection = LayoutDirection.Ltr,
-            fontFamilyResolver = mock(),
-            constraints = mock()
-        )
-        whenever(textLayoutResult.layoutInput).thenReturn(input)
-
-        TextFieldDelegate.notifyFocusedRect(
-            editorState,
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            OffsetMapping.Identity
-        )
-        verify(textInputSession).notifyFocusedRect(any())
-    }
-
-    @Test
-    fun notify_focused_rect_without_focus() {
-        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
-        val textInputSession: TextInputSession = mock()
-        TextFieldDelegate.notifyFocusedRect(
-            editorState,
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            false /* hasFocus */,
-            OffsetMapping.Identity
-        )
-        verify(textInputSession, never()).notifyFocusedRect(any())
-    }
-
-    @Test
-    fun notify_rect_tail() {
-        val rect = Rect(0f, 1f, 2f, 3f)
-        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
-        val point = Offset(5f, 6f)
-        layoutCoordinates = MockCoordinates(
-            rootOffset = point
-        )
-        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(12))
-        val textInputSession: TextInputSession = mock()
-        val input = TextLayoutInput(
-            text = AnnotatedString(editorState.text),
-            style = TextStyle(),
-            placeholders = listOf(),
-            maxLines = Int.MAX_VALUE,
-            softWrap = true,
-            overflow = TextOverflow.Clip,
-            density = Density(1.0f),
-            layoutDirection = LayoutDirection.Ltr,
-            fontFamilyResolver = mock(),
-            constraints = mock()
-        )
-        whenever(textLayoutResult.layoutInput).thenReturn(input)
-
-        TextFieldDelegate.notifyFocusedRect(
-            editorState,
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            OffsetMapping.Identity
-        )
-        verify(textInputSession).notifyFocusedRect(any())
-    }
-
-    @Test
-    fun check_notify_rect_uses_offset_map() {
-        val rect = Rect(0f, 1f, 2f, 3f)
-        val point = Offset(5f, 6f)
-        val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1, 3))
-
-        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
-        val input = TextLayoutInput(
-            text = AnnotatedString(editorState.text),
-            style = TextStyle(),
-            placeholders = listOf(),
-            maxLines = Int.MAX_VALUE,
-            softWrap = true,
-            overflow = TextOverflow.Clip,
-            density = Density(1.0f),
-            layoutDirection = LayoutDirection.Ltr,
-            fontFamilyResolver = mock(),
-            constraints = mock()
-        )
-        whenever(textLayoutResult.layoutInput).thenReturn(input)
-        layoutCoordinates = MockCoordinates(
-            rootOffset = point
-        )
-        val textInputSession: TextInputSession = mock()
-
-        TextFieldDelegate.notifyFocusedRect(
-            editorState,
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            skippingOffsetMap
-        )
-        verify(textLayoutResult).getBoundingBox(6)
-        verify(textInputSession).notifyFocusedRect(any())
-    }
-
-    @Test
     fun check_setCursorOffset_uses_offset_map() {
         val position = Offset(100f, 200f)
         val offset = 10
@@ -405,106 +268,4 @@
             )
         )
     }
-
-    @Test
-    fun notify_transformed_text() {
-        val rect = Rect(0f, 1f, 2f, 3f)
-        whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
-        val point = Offset(5f, 6f)
-        layoutCoordinates = MockCoordinates(
-            rootOffset = point
-        )
-
-        val textInputSession: TextInputSession = mock()
-        val input = TextLayoutInput(
-            // In this test case, transform the text into double characters text.
-            text = AnnotatedString("HHeelllloo,,  WWoorrlldd"),
-            style = TextStyle(),
-            placeholders = listOf(),
-            maxLines = Int.MAX_VALUE,
-            softWrap = true,
-            overflow = TextOverflow.Clip,
-            density = Density(1.0f),
-            layoutDirection = LayoutDirection.Ltr,
-            fontFamilyResolver = mock(),
-            constraints = mock()
-        )
-        whenever(textLayoutResult.layoutInput).thenReturn(input)
-
-        val offsetMapping = object : OffsetMapping {
-            override fun originalToTransformed(offset: Int): Int = offset * 2
-            override fun transformedToOriginal(offset: Int): Int = offset / 2
-        }
-
-        // The beginning of the text.
-        TextFieldDelegate.notifyFocusedRect(
-            TextFieldValue(text = "Hello, World", selection = TextRange(0)),
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            offsetMapping
-        )
-        verify(textInputSession).notifyFocusedRect(any())
-
-        // The tail of the transformed text.
-        reset(textInputSession)
-        TextFieldDelegate.notifyFocusedRect(
-            TextFieldValue(text = "Hello, World", selection = TextRange(24)),
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            offsetMapping
-        )
-        verify(textInputSession).notifyFocusedRect(any())
-
-        // Beyond the tail of the transformed text.
-        reset(textInputSession)
-        TextFieldDelegate.notifyFocusedRect(
-            TextFieldValue(text = "Hello, World", selection = TextRange(25)),
-            mDelegate,
-            textLayoutResult,
-            layoutCoordinates,
-            textInputSession,
-            true /* hasFocus */,
-            offsetMapping
-        )
-        verify(textInputSession).notifyFocusedRect(any())
-    }
-
-    private class MockCoordinates(
-        override val size: IntSize = IntSize.Zero,
-        val localOffset: Offset = Offset.Zero,
-        val globalOffset: Offset = Offset.Zero,
-        val rootOffset: Offset = Offset.Zero
-    ) : LayoutCoordinates {
-        override val providedAlignmentLines: Set<AlignmentLine>
-            get() = emptySet()
-        override val parentLayoutCoordinates: LayoutCoordinates?
-            get() = null
-        override val parentCoordinates: LayoutCoordinates?
-            get() = null
-        override val isAttached: Boolean
-            get() = true
-
-        override fun windowToLocal(relativeToWindow: Offset): Offset = localOffset
-
-        override fun localToWindow(relativeToLocal: Offset): Offset = globalOffset
-
-        override fun localToRoot(relativeToLocal: Offset): Offset = rootOffset
-        override fun localPositionOf(
-            sourceCoordinates: LayoutCoordinates,
-            relativeToSource: Offset
-        ): Offset = Offset.Zero
-
-        override fun localBoundingBoxOf(
-            sourceCoordinates: LayoutCoordinates,
-            clipBounds: Boolean
-        ): Rect = Rect.Zero
-
-        override fun get(alignmentLine: AlignmentLine): Int = 0
-    }
 }
\ No newline at end of file
diff --git a/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt b/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
index 89dda08..24c5f18 100644
--- a/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
+++ b/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
@@ -16,39 +16,42 @@
 
 package androidx.compose.material.catalog.ui.specification
 
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.catalog.model.Specification
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SpecificationItem(
     specification: Specification,
     onClick: (specification: Specification) -> Unit
 ) {
-    // TODO: Replace with M3 Card when available
-    Surface(
-        onClick = { onClick(specification) },
-        modifier = Modifier.fillMaxWidth(),
-        shape = SpecificationItemShape,
-        border = BorderStroke(
-            width = SpecificationItemBorderWidth,
-            color = MaterialTheme.colorScheme.outline
-        )
+    val interactionSource = remember { MutableInteractionSource() }
+    OutlinedCard(
+        modifier = Modifier
+            .fillMaxWidth()
+            .clickable(
+                interactionSource = interactionSource,
+                indication = null,
+                onClick = { onClick(specification) }),
+        interactionSource = interactionSource
     ) {
         Row(
             modifier = Modifier.padding(SpecificationItemPadding),
@@ -79,5 +82,3 @@
 
 private val SpecificationItemPadding = 16.dp
 private val SpecificationItemTextPadding = 8.dp
-private val SpecificationItemBorderWidth = 1.dp
-private val SpecificationItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index fcfc11e..c3e5a1a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -8,6 +8,11 @@
     method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
   }
 
+  public final class AndroidMenu_androidKt {
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
   public final class AppBarKt {
     method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -218,6 +223,22 @@
     method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class MenuDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+    property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+    field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Stable public interface MenuItemColors {
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+  }
+
+  public final class MenuKt {
+  }
+
   @androidx.compose.runtime.Stable public interface NavigationBarItemColors {
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -333,7 +354,7 @@
 
   public final class SurfaceKt {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
   }
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 4f97112..0037ea0 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -8,6 +8,11 @@
     method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
   }
 
+  public final class AndroidMenu_androidKt {
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
   public final class AppBarKt {
     method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -274,6 +279,22 @@
     method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class MenuDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+    property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+    field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Stable public interface MenuItemColors {
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+  }
+
+  public final class MenuKt {
+  }
+
   @androidx.compose.runtime.Stable public interface NavigationBarItemColors {
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -394,7 +415,7 @@
 
   public final class SurfaceKt {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
   }
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index fcfc11e..c3e5a1a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -8,6 +8,11 @@
     method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
   }
 
+  public final class AndroidMenu_androidKt {
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
   public final class AppBarKt {
     method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -218,6 +223,22 @@
     method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class MenuDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+    property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+    field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Stable public interface MenuItemColors {
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+  }
+
+  public final class MenuKt {
+  }
+
   @androidx.compose.runtime.Stable public interface NavigationBarItemColors {
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -333,7 +354,7 @@
 
   public final class SurfaceKt {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
   }
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 3d50af6..3ee98f8 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -131,6 +131,18 @@
     examples = FloatingActionButtonsExamples,
 )
 
+private val Menus = Component(
+    id = nextId(),
+    name = "Menus",
+    description = "Menus display a list of choices on temporary surfaces.",
+    // No menu icon
+    tintIcon = true,
+    guidelinesUrl = "$ComponentGuidelinesUrl/menus",
+    docsUrl = "$PackageSummaryUrl#dropdownmenu",
+    sourceUrl = "$Material3SourceUrl/Menu.kt",
+    examples = MenusExamples
+)
+
 private val NavigationBar = Component(
     id = nextId(),
     name = "Navigation bar",
@@ -228,6 +240,7 @@
     Dialogs,
     ExtendedFloatingActionButton,
     FloatingActionButtons,
+    Menus,
     NavigationBar,
     NavigationDrawer,
     NavigationRail,
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 450590ab..3ee5792 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -40,6 +40,7 @@
 import androidx.compose.material3.samples.FloatingActionButtonSample
 import androidx.compose.material3.samples.LargeFloatingActionButtonSample
 import androidx.compose.material3.samples.LinearProgressIndicatorSample
+import androidx.compose.material3.samples.MenuSample
 import androidx.compose.material3.samples.NavigationBarSample
 import androidx.compose.material3.samples.NavigationBarWithOnlySelectedLabelsSample
 import androidx.compose.material3.samples.NavigationDrawerSample
@@ -267,6 +268,18 @@
         ) { SmallFloatingActionButtonSample() }
     )
 
+private const val MenusExampleDescription = "Menus examples"
+private const val MenusExampleSourceUrl = "$SampleSourceUrl/MenuSamples.kt"
+val MenusExamples = listOf(
+    Example(
+        name = ::MenuSample.name,
+        description = MenusExampleDescription,
+        sourceUrl = MenusExampleSourceUrl
+    ) {
+        MenuSample()
+    }
+)
+
 private const val NavigationBarExampleDescription = "Navigation bar examples"
 private const val NavigationBarExampleSourceUrl = "$SampleSourceUrl/NavigationBarSamples.kt"
 val NavigationBarExamples =
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
index 0576bae..f399060 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
@@ -16,19 +16,22 @@
 
 package androidx.compose.material3.catalog.library.ui.component
 
-import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
 import androidx.compose.material3.Text
 import androidx.compose.material3.catalog.library.model.Component
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
@@ -36,24 +39,24 @@
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun ComponentItem(
     component: Component,
     onClick: (component: Component) -> Unit
 ) {
-    // TODO: Replace with M3 Card when available
-    Surface(
-        onClick = { onClick(component) },
+    val interactionSource = remember { MutableInteractionSource() }
+    OutlinedCard(
         modifier = Modifier
             .height(ComponentItemHeight)
-            .padding(ComponentItemOuterPadding),
-        shape = ComponentItemShape,
-        border = BorderStroke(
-            width = ComponentItemBorderWidth,
-            color = MaterialTheme.colorScheme.outline
-        )
+            .padding(ComponentItemOuterPadding)
+            .clickable(
+                interactionSource = interactionSource,
+                indication = null,
+                onClick = { onClick(component) }),
+        interactionSource = interactionSource
     ) {
-        Box(modifier = Modifier.padding(ComponentItemInnerPadding)) {
+        Box(modifier = Modifier.fillMaxSize().padding(ComponentItemInnerPadding)) {
             Image(
                 painter = painterResource(id = component.icon),
                 contentDescription = null,
@@ -80,5 +83,3 @@
 private val ComponentItemOuterPadding = 4.dp
 private val ComponentItemInnerPadding = 16.dp
 private val ComponentItemIconSize = 80.dp
-private val ComponentItemBorderWidth = 1.dp
-private val ComponentItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
index e8d005e..807d84a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
@@ -16,7 +16,8 @@
 
 package androidx.compose.material3.catalog.library.ui.example
 
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -24,33 +25,35 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
 import androidx.compose.material3.Text
 import androidx.compose.material3.catalog.library.model.Example
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun ExampleItem(
     example: Example,
     onClick: (example: Example) -> Unit
 ) {
-    // TODO: Replace with M3 Card when available
-    Surface(
-        onClick = { onClick(example) },
-        modifier = Modifier.fillMaxWidth(),
-        shape = ExampleItemShape,
-        border = BorderStroke(
-            width = ExampleItemBorderWidth,
-            color = MaterialTheme.colorScheme.outline
-        )
+    val interactionSource = remember { MutableInteractionSource() }
+    OutlinedCard(
+        modifier = Modifier
+            .fillMaxWidth()
+            .clickable(
+                interactionSource = interactionSource,
+                indication = null,
+                onClick = { onClick(example) }),
+        interactionSource = interactionSource
     ) {
         Row(modifier = Modifier.padding(ExampleItemPadding)) {
             Column(modifier = Modifier.weight(1f, fill = true)) {
@@ -76,5 +79,3 @@
 
 private val ExampleItemPadding = 16.dp
 private val ExampleItemTextPadding = 8.dp
-private val ExampleItemBorderWidth = 1.dp
-private val ExampleItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt
new file mode 100644
index 0000000..2455922
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Email
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.Divider
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+
+@Sampled
+@Composable
+fun MenuSample() {
+    var expanded by remember { mutableStateOf(false) }
+
+    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+        IconButton(onClick = { expanded = true }) {
+            Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+        }
+        DropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false }
+        ) {
+            DropdownMenuItem(
+                text = { Text("Edit") },
+                onClick = { /* Handle edit! */ },
+                leadingIcon = {
+                    Icon(
+                        Icons.Outlined.Edit,
+                        contentDescription = null
+                    )
+                })
+            DropdownMenuItem(
+                text = { Text("Settings") },
+                onClick = { /* Handle settings! */ },
+                leadingIcon = {
+                    Icon(
+                        Icons.Outlined.Settings,
+                        contentDescription = null
+                    )
+                })
+            Divider()
+            DropdownMenuItem(
+                text = { Text("Send Feedback") },
+                onClick = { /* Handle send feedback! */ },
+                leadingIcon = {
+                    Icon(
+                        Icons.Outlined.Email,
+                        contentDescription = null
+                    )
+                },
+                trailingIcon = { Text("F11", textAlign = TextAlign.Center) })
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
new file mode 100644
index 0000000..ea3f79f
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 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
+
+import android.os.Build
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Email
+import androidx.compose.material.icons.outlined.Lock
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Screenshot tests for the Material Menus.
+ *
+ * Note that currently nodes in a popup cannot be captured to bitmaps. A [DropdownMenu] is
+ * displaying its content in a popup, so the tests here focus on the [DropdownMenuContent].
+ */
+// TODO(b/208991956): Update to include DropdownMenu when popups can be captured into bitmaps.
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class MenuScreenshotTest {
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    private val testTag = "dropdown_menu"
+
+    @Test
+    fun dropdownMenu_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            TestMenu(enabledItems = true)
+        }
+        assertAgainstGolden(goldenIdentifier = "dropdownMenu_lightTheme")
+    }
+
+    @Test
+    fun dropdownMenu_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            TestMenu(enabledItems = true)
+        }
+        assertAgainstGolden(goldenIdentifier = "dropdownMenu_darkTheme")
+    }
+
+    @Test
+    fun dropdownMenu_disabled_lightTheme() {
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            TestMenu(enabledItems = false)
+        }
+        assertAgainstGolden(goldenIdentifier = "dropdownMenu_disabled_lightTheme")
+    }
+
+    @Test
+    fun dropdownMenu_disabled_darkTheme() {
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            TestMenu(enabledItems = false)
+        }
+        assertAgainstGolden(goldenIdentifier = "dropdownMenu_disabled_darkTheme")
+    }
+
+    @Composable
+    private fun TestMenu(enabledItems: Boolean) {
+        Box(Modifier.testTag(testTag).padding(20.dp), contentAlignment = Alignment.Center) {
+            DropdownMenuContent(
+                expandedStates = MutableTransitionState(initialState = true),
+                transformOriginState = mutableStateOf(TransformOrigin.Center)
+            ) {
+                DropdownMenuItem(
+                    text = { Text("Edit") },
+                    onClick = { },
+                    enabled = enabledItems,
+                    leadingIcon = {
+                        Icon(
+                            Icons.Outlined.Edit,
+                            contentDescription = null
+                        )
+                    })
+                DropdownMenuItem(
+                    text = { Text("Settings") },
+                    onClick = { },
+                    enabled = enabledItems,
+                    leadingIcon = {
+                        Icon(
+                            Icons.Outlined.Settings,
+                            contentDescription = null
+                        )
+                    },
+                    trailingIcon = { Text("F11", textAlign = TextAlign.Center) })
+                Divider()
+                DropdownMenuItem(
+                    text = { Text("Send Feedback") },
+                    onClick = { },
+                    enabled = enabledItems,
+                    leadingIcon = {
+                        Icon(
+                            Icons.Outlined.Email,
+                            contentDescription = null
+                        )
+                    },
+                    trailingIcon = {
+                        Icon(
+                            Icons.Outlined.Lock,
+                            contentDescription = null
+                        )
+                    })
+            }
+        }
+    }
+
+    private fun assertAgainstGolden(goldenIdentifier: String) {
+        composeTestRule.onNodeWithTag(testTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenIdentifier)
+    }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
new file mode 100644
index 0000000..124eac9
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2022 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
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+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)
+@OptIn(ExperimentalTestApi::class)
+class MenuTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun menu_canBeTriggered() {
+        var expanded by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.requiredSize(20.dp).background(color = Color.Blue)) {
+                DropdownMenu(
+                    expanded = expanded,
+                    onDismissRequest = {}
+                ) {
+                    DropdownMenuItem(
+                        text = { Text("Option 1") },
+                        modifier = Modifier.testTag("MenuContent"),
+                        onClick = {})
+                }
+            }
+        }
+
+        rule.onNodeWithTag("MenuContent").assertDoesNotExist()
+        rule.mainClock.autoAdvance = false
+
+        rule.runOnUiThread { expanded = true }
+        rule.mainClock.advanceTimeByFrame() // Trigger the popup
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame() // Kick off the animation
+        rule.mainClock.advanceTimeBy(InTransitionDuration.toLong())
+        rule.onNodeWithTag("MenuContent").assertExists()
+
+        rule.runOnUiThread { expanded = false }
+        rule.mainClock.advanceTimeByFrame() // Trigger the popup
+        rule.mainClock.advanceTimeByFrame() // Kick off the animation
+        rule.mainClock.advanceTimeBy(OutTransitionDuration.toLong())
+        rule.mainClock.advanceTimeByFrame()
+        rule.onNodeWithTag("MenuContent").assertDoesNotExist()
+
+        rule.runOnUiThread { expanded = true }
+        rule.mainClock.advanceTimeByFrame() // Trigger the popup
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame() // Kick off the animation
+        rule.mainClock.advanceTimeBy(InTransitionDuration.toLong())
+        rule.onNodeWithTag("MenuContent").assertExists()
+    }
+
+    @Test
+    fun menu_hasExpectedSize() {
+        rule.setContent {
+            with(LocalDensity.current) {
+                Box(Modifier.requiredSize(20.toDp()).background(color = Color.Blue)) {
+                    DropdownMenu(
+                        expanded = true,
+                        onDismissRequest = {}
+                    ) {
+                        Box(Modifier.testTag("MenuContent1").size(70.toDp()))
+                        Box(Modifier.testTag("MenuContent2").size(130.toDp()))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("MenuContent1").assertExists()
+        rule.onNodeWithTag("MenuContent2").assertExists()
+        val node = rule.onNode(
+            isPopup() and hasAnyDescendant(hasTestTag("MenuContent1")) and
+                hasAnyDescendant(hasTestTag("MenuContent2"))
+        ).assertExists().fetchSemanticsNode()
+        with(rule.density) {
+            assertThat(node.size.width).isEqualTo(130)
+            assertThat(node.size.height)
+                .isEqualTo(DropdownMenuVerticalPadding.roundToPx() * 2 + 200)
+        }
+    }
+
+    @Test
+    fun menu_positioning_bottomEnd() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowSize = IntSize(screenWidth, screenHeight)
+        val anchorPosition = IntOffset(100, 200)
+        val anchorSize = IntSize(10, 20)
+        val offsetX = 20
+        val offsetY = 40
+        val popupSize = IntSize(50, 80)
+
+        val ltrPosition = DropdownMenuPositionProvider(
+            DpOffset(offsetX.dp, offsetY.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(ltrPosition.x).isEqualTo(
+            anchorPosition.x + offsetX
+        )
+        assertThat(ltrPosition.y).isEqualTo(
+            anchorPosition.y + anchorSize.height + offsetY
+        )
+
+        val rtlPosition = DropdownMenuPositionProvider(
+            DpOffset(offsetX.dp, offsetY.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Rtl,
+            popupSize
+        )
+
+        assertThat(rtlPosition.x).isEqualTo(
+            anchorPosition.x + anchorSize.width - offsetX - popupSize.width
+        )
+        assertThat(rtlPosition.y).isEqualTo(
+            anchorPosition.y + anchorSize.height + offsetY
+        )
+    }
+
+    @Test
+    fun menu_positioning_topStart() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowSize = IntSize(screenWidth, screenHeight)
+        val anchorPosition = IntOffset(450, 950)
+        val anchorPositionRtl = IntOffset(50, 950)
+        val anchorSize = IntSize(10, 20)
+        val offsetX = 20
+        val offsetY = 40
+        val popupSize = IntSize(150, 80)
+
+        val ltrPosition = DropdownMenuPositionProvider(
+            DpOffset(offsetX.dp, offsetY.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(ltrPosition.x).isEqualTo(
+            anchorPosition.x + anchorSize.width - offsetX - popupSize.width
+        )
+        assertThat(ltrPosition.y).isEqualTo(
+            anchorPosition.y - popupSize.height - offsetY
+        )
+
+        val rtlPosition = DropdownMenuPositionProvider(
+            DpOffset(offsetX.dp, offsetY.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPositionRtl, anchorSize),
+            windowSize,
+            LayoutDirection.Rtl,
+            popupSize
+        )
+
+        assertThat(rtlPosition.x).isEqualTo(
+            anchorPositionRtl.x + offsetX
+        )
+        assertThat(rtlPosition.y).isEqualTo(
+            anchorPositionRtl.y - popupSize.height - offsetY
+        )
+    }
+
+    @Test
+    fun menu_positioning_top() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowSize = IntSize(screenWidth, screenHeight)
+        val anchorPosition = IntOffset(0, 0)
+        val anchorSize = IntSize(50, 20)
+        val popupSize = IntSize(150, 500)
+
+        // The min margin above and below the menu, relative to the screen.
+        val menuVerticalMargin = 48.dp
+        val verticalMargin = with(density) { menuVerticalMargin.roundToPx() }
+
+        val position = DropdownMenuPositionProvider(
+            DpOffset(0.dp, 0.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(position.y).isEqualTo(
+            verticalMargin
+        )
+    }
+
+    @Test
+    fun menu_positioning_anchorPartiallyVisible() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowSize = IntSize(screenWidth, screenHeight)
+        val anchorPosition = IntOffset(-25, -10)
+        val anchorPositionRtl = IntOffset(525, -10)
+        val anchorSize = IntSize(50, 20)
+        val popupSize = IntSize(150, 500)
+
+        // The min margin above and below the menu, relative to the screen.
+        val menuVerticalMargin = 48.dp
+        val verticalMargin = with(density) { menuVerticalMargin.roundToPx() }
+
+        val position = DropdownMenuPositionProvider(
+            DpOffset(0.dp, 0.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(position.x).isEqualTo(
+            0
+        )
+        assertThat(position.y).isEqualTo(
+            verticalMargin
+        )
+
+        val rtlPosition = DropdownMenuPositionProvider(
+            DpOffset(0.dp, 0.dp),
+            density
+        ).calculatePosition(
+            IntRect(anchorPositionRtl, anchorSize),
+            windowSize,
+            LayoutDirection.Rtl,
+            popupSize
+        )
+
+        assertThat(rtlPosition.x).isEqualTo(
+            screenWidth - popupSize.width
+        )
+        assertThat(rtlPosition.y).isEqualTo(
+            verticalMargin
+        )
+    }
+
+    @Test
+    fun menu_positioning_callback() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowSize = IntSize(screenWidth, screenHeight)
+        val anchorPosition = IntOffset(100, 200)
+        val anchorSize = IntSize(10, 20)
+        val offsetX = 20
+        val offsetY = 40
+        val popupSize = IntSize(50, 80)
+
+        var obtainedParentBounds = IntRect(0, 0, 0, 0)
+        var obtainedMenuBounds = IntRect(0, 0, 0, 0)
+        DropdownMenuPositionProvider(
+            DpOffset(offsetX.dp, offsetY.dp),
+            density
+        ) { parentBounds, menuBounds ->
+            obtainedParentBounds = parentBounds
+            obtainedMenuBounds = menuBounds
+        }.calculatePosition(
+            IntRect(anchorPosition, anchorSize),
+            windowSize,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(obtainedParentBounds).isEqualTo(IntRect(anchorPosition, anchorSize))
+        assertThat(obtainedMenuBounds).isEqualTo(
+            IntRect(
+                anchorPosition.x + offsetX,
+                anchorPosition.y + anchorSize.height + offsetY,
+                anchorPosition.x + offsetX + popupSize.width,
+                anchorPosition.y + anchorSize.height + offsetY + popupSize.height
+            )
+        )
+    }
+
+    @Test
+    fun dropdownMenuItem_onClick() {
+        var clicked = false
+        val onClick: () -> Unit = { clicked = true }
+
+        rule.setContent {
+            DropdownMenuItem(
+                text = { Box(Modifier.requiredSize(40.dp)) },
+                onClick,
+                modifier = Modifier.testTag("MenuItem").clickable(onClick = onClick),
+            )
+        }
+
+        rule.onNodeWithTag("MenuItem").performClick()
+
+        rule.runOnIdle {
+            assertThat(clicked).isTrue()
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
index 0d52061..265b771 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
@@ -307,28 +307,7 @@
     }
 
     @Test
-    fun clickableOverload_clickAction() {
-        val count = mutableStateOf(0f)
-        rule.setMaterialContent(lightColorScheme()) {
-            Surface(
-                modifier = Modifier.testTag("surface"),
-                onClick = { count.value += 1 }
-            ) {
-                Spacer(Modifier.size(30.dp))
-            }
-        }
-        rule.onNodeWithTag("surface")
-            .performClick()
-        Truth.assertThat(count.value).isEqualTo(1)
-
-        rule.onNodeWithTag("surface")
-            .performClick()
-            .performClick()
-        Truth.assertThat(count.value).isEqualTo(3)
-    }
-
-    @Test
-    fun clickable_clickActionWithModifier() {
+    fun clickable_clickAction() {
         val count = mutableStateOf(0f)
         val interactionSource = MutableInteractionSource()
         rule.setMaterialContent(lightColorScheme()) {
@@ -382,7 +361,7 @@
     }
 
     @Test
-    fun clickableOverload_interactionSource() {
+    fun clickable_interactionSource() {
         val interactionSource = MutableInteractionSource()
 
         var scope: CoroutineScope? = null
@@ -392,8 +371,12 @@
         rule.setContent {
             scope = rememberCoroutineScope()
             Surface(
-                modifier = Modifier.testTag("surface"),
-                onClick = {},
+                modifier =
+                Modifier.testTag("surface")
+                    .clickable(
+                        interactionSource = interactionSource,
+                        indication = null,
+                        onClick = {}),
                 interactionSource = interactionSource
             ) {
                 Spacer(Modifier.size(30.dp))
@@ -433,7 +416,24 @@
         }
     }
 
-    // TODO(b/198216553): Add surface_blockClicksBehind test from M2 after Button is added.
+    @Test
+    fun surface_blockClicksBehind() {
+        val state = mutableStateOf(0)
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Button(
+                    modifier = Modifier.fillMaxSize().testTag("clickable"),
+                    onClick = { state.value += 1 }
+                ) { Text("button fullscreen") }
+                Surface(
+                    Modifier.fillMaxSize().testTag("surface"),
+                ) {}
+            }
+        }
+        rule.onNodeWithTag("clickable").assertHasClickAction().performClick()
+        // still 0
+        Truth.assertThat(state.value).isEqualTo(0)
+    }
 
     // regression test for b/189411183
     @Test
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
new file mode 100644
index 0000000..159f821
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 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
+
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+
+/**
+ * <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material3.samples.MenuSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param offset [DpOffset] to be added to the position of the menu
+ */
+@Suppress("ModifierParameter")
+@Composable
+fun DropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    properties: PopupProperties = PopupProperties(focusable = true),
+    content: @Composable ColumnScope.() -> Unit
+) {
+    val expandedStates = remember { MutableTransitionState(false) }
+    expandedStates.targetState = expanded
+
+    if (expandedStates.currentState || expandedStates.targetState) {
+        val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
+        val density = LocalDensity.current
+        val popupPositionProvider = DropdownMenuPositionProvider(
+            offset,
+            density
+        ) { parentBounds, menuBounds ->
+            transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
+        }
+
+        Popup(
+            onDismissRequest = onDismissRequest,
+            popupPositionProvider = popupPositionProvider,
+            properties = properties
+        ) {
+            DropdownMenuContent(
+                expandedStates = expandedStates,
+                transformOriginState = transformOriginState,
+                modifier = modifier,
+                content = content
+            )
+        }
+    }
+}
+
+/**
+ * <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a> item.
+ *
+ *
+ * Example usage:
+ * @sample androidx.compose.material3.samples.MenuSample
+ *
+ * @param text The menu item text
+ * @param onClick Called when the menu item was clicked
+ * @param modifier The modifier to be applied to the menu item
+ * @param leadingIcon Optional leading icon to be displayed at the beginning of the item's text
+ * @param trailingIcon Optional trailing icon to be displayed at the end of the item's text. This
+ * trailing icon slot can also accept [Text] to indicate a keyboard shortcut, for example.
+ * @param enabled Controls the enabled state of the menu item - when `false`, the menu item
+ * will not be clickable and [onClick] will not be invoked
+ * @param colors [MenuItemColors] that will be used to resolve the background and content color for
+ * this item in different states. See [MenuDefaults.itemColors].
+ * @param contentPadding the padding applied to the content of this menu item
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this DropdownMenuItem. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this DropdownMenuItem in different [Interaction]s.
+ */
+@Composable
+fun DropdownMenuItem(
+    text: @Composable () -> Unit,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    enabled: Boolean = true,
+    colors: MenuItemColors = MenuDefaults.itemColors(),
+    contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+    DropdownMenuItemContent(
+        text = text,
+        onClick = onClick,
+        modifier = modifier,
+        leadingIcon = leadingIcon,
+        trailingIcon = trailingIcon,
+        enabled = enabled,
+        colors = colors,
+        contentPadding = contentPadding,
+        interactionSource = interactionSource,
+    )
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 9613eed..6b44cef 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
@@ -31,7 +32,6 @@
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.padding
-import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.material3.tokens.ElevatedButtonTokens
 import androidx.compose.material3.tokens.FilledButtonTokens
 import androidx.compose.material3.tokens.FilledButtonTonalTokens
@@ -119,19 +119,23 @@
 
     // TODO(b/202880001): Apply shadow color from token (will not be possibly any time soon, if ever).
     Surface(
-        modifier = modifier.minimumTouchTargetSize(),
+        modifier = modifier
+            .minimumTouchTargetSize()
+            .clickable(
+                interactionSource = interactionSource,
+                indication = null,
+                enabled = enabled,
+                role = Role.Button,
+                onClick = onClick
+            ),
+        interactionSource = interactionSource,
         shape = shape,
         color = containerColor,
         contentColor = contentColor,
-        shadowElevation = shadowElevation,
         tonalElevation = tonalElevation,
-        border = border,
-        onClick = onClick,
-        enabled = enabled,
-        role = Role.Button,
-        interactionSource = interactionSource,
-        indication = rememberRipple(),
-        ) {
+        shadowElevation = shadowElevation,
+        border = border
+    ) {
         CompositionLocalProvider(LocalContentColor provides contentColor) {
             ProvideTextStyle(value = TypographyTokens.LabelLarge) {
                 Row(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
new file mode 100644
index 0000000..4a357c8
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2022 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
+
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.tokens.MenuTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.PopupPositionProvider
+import kotlin.math.max
+import kotlin.math.min
+
+@Suppress("ModifierParameter")
+@Composable
+internal fun DropdownMenuContent(
+    expandedStates: MutableTransitionState<Boolean>,
+    transformOriginState: MutableState<TransformOrigin>,
+    modifier: Modifier = Modifier,
+    content: @Composable ColumnScope.() -> Unit
+) {
+    // Menu open/close animation.
+    val transition = updateTransition(expandedStates, "DropDownMenu")
+
+    val scale by transition.animateFloat(
+        transitionSpec = {
+            if (false isTransitioningTo true) {
+                // Dismissed to expanded
+                tween(
+                    durationMillis = InTransitionDuration,
+                    easing = LinearOutSlowInEasing
+                )
+            } else {
+                // Expanded to dismissed.
+                tween(
+                    durationMillis = 1,
+                    delayMillis = OutTransitionDuration - 1
+                )
+            }
+        }
+    ) {
+        if (it) {
+            // Menu is expanded.
+            1f
+        } else {
+            // Menu is dismissed.
+            0.8f
+        }
+    }
+
+    val alpha by transition.animateFloat(
+        transitionSpec = {
+            if (false isTransitioningTo true) {
+                // Dismissed to expanded
+                tween(durationMillis = 30)
+            } else {
+                // Expanded to dismissed.
+                tween(durationMillis = OutTransitionDuration)
+            }
+        }
+    ) {
+        if (it) {
+            // Menu is expanded.
+            1f
+        } else {
+            // Menu is dismissed.
+            0f
+        }
+    }
+    Surface(
+        modifier = Modifier.graphicsLayer {
+            scaleX = scale
+            scaleY = scale
+            this.alpha = alpha
+            transformOrigin = transformOriginState.value
+        },
+        shape = MenuTokens.ContainerShape,
+        color = MaterialTheme.colorScheme.fromToken(MenuTokens.ContainerColor),
+        tonalElevation = MenuTokens.ContainerElevation,
+        shadowElevation = MenuTokens.ContainerElevation
+    ) {
+        Column(
+            modifier = modifier
+                .padding(vertical = DropdownMenuVerticalPadding)
+                .width(IntrinsicSize.Max)
+                .verticalScroll(rememberScrollState()),
+            content = content
+        )
+    }
+}
+
+@Composable
+internal fun DropdownMenuItemContent(
+    text: @Composable () -> Unit,
+    onClick: () -> Unit,
+    modifier: Modifier,
+    leadingIcon: @Composable (() -> Unit)?,
+    trailingIcon: @Composable (() -> Unit)?,
+    enabled: Boolean,
+    colors: MenuItemColors,
+    contentPadding: PaddingValues,
+    interactionSource: MutableInteractionSource
+) {
+    Row(
+        modifier = modifier
+            .clickable(
+                enabled = enabled,
+                onClick = onClick,
+                interactionSource = interactionSource,
+                indication = rememberRipple(true)
+            )
+            .fillMaxWidth()
+            // Preferred min and max width used during the intrinsic measurement.
+            .sizeIn(
+                minWidth = DropdownMenuItemDefaultMinWidth,
+                maxWidth = DropdownMenuItemDefaultMaxWidth,
+                minHeight = MenuTokens.ListItemContainerHeight
+            )
+            .padding(contentPadding),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        ProvideTextStyle(MaterialTheme.typography.fromToken(MenuTokens.ListItemLabelTextFont)) {
+            if (leadingIcon != null) {
+                CompositionLocalProvider(
+                    LocalContentColor provides colors.leadingIconColor(enabled).value,
+                ) {
+                    Box(Modifier.defaultMinSize(minWidth = MenuTokens.ListItemLeadingIconSize)) {
+                        leadingIcon()
+                    }
+                }
+            }
+            CompositionLocalProvider(LocalContentColor provides colors.textColor(enabled).value) {
+                Box(
+                    Modifier.weight(1f)
+                        .padding(
+                            start = if (leadingIcon != null) {
+                                DropdownMenuItemHorizontalPadding
+                            } else {
+                                0.dp
+                            },
+                            end = if (trailingIcon != null) {
+                                DropdownMenuItemHorizontalPadding
+                            } else {
+                                0.dp
+                            }
+                        )
+                ) {
+                    text()
+                }
+            }
+            if (trailingIcon != null) {
+                CompositionLocalProvider(
+                    LocalContentColor provides colors.trailingIconColor(enabled).value
+                ) {
+                    Box(Modifier.defaultMinSize(minWidth = MenuTokens.ListItemTrailingIconSize)) {
+                        trailingIcon()
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Contains default values used for [DropdownMenuItem].
+ */
+object MenuDefaults {
+
+    /**
+     * Creates a [MenuItemColors] that represents the default text and icon colors used in a
+     * [DropdownMenuItemContent].
+     *
+     * @param textColor the text color of this [DropdownMenuItemContent] when enabled
+     * @param leadingIconColor the leading icon color of this [DropdownMenuItemContent] when enabled
+     * @param trailingIconColor the trailing icon color of this [DropdownMenuItemContent] when
+     * enabled
+     * @param disabledTextColor the text color of this [DropdownMenuItemContent] when not enabled
+     * @param disabledLeadingIconColor the leading icon color of this [DropdownMenuItemContent] when
+     * not enabled
+     * @param disabledTrailingIconColor the trailing icon color of this [DropdownMenuItemContent]
+     * when not enabled
+     */
+    @Composable
+    fun itemColors(
+        textColor: Color = MenuTokens.ListItemLabelTextColor.toColor(),
+        leadingIconColor: Color = MenuTokens.ListItemLeadingIconColor.toColor(),
+        trailingIconColor: Color = MenuTokens.ListItemTrailingIconColor.toColor(),
+        disabledTextColor: Color =
+            MenuTokens.ListItemDisabledLabelTextColor.toColor()
+                .copy(alpha = MenuTokens.ListItemDisabledLabelTextOpacity),
+        disabledLeadingIconColor: Color = MenuTokens.ListItemDisabledLeadingIconColor.toColor()
+            .copy(alpha = MenuTokens.ListItemDisabledLeadingIconOpacity),
+        disabledTrailingIconColor: Color = MenuTokens.ListItemDisabledTrailingIconColor.toColor()
+            .copy(alpha = MenuTokens.ListItemDisabledTrailingIconOpacity),
+    ): MenuItemColors =
+        DefaultMenuItemColors(
+            textColor = textColor,
+            leadingIconColor = leadingIconColor,
+            trailingIconColor = trailingIconColor,
+            disabledTextColor = disabledTextColor,
+            disabledLeadingIconColor = disabledLeadingIconColor,
+            disabledTrailingIconColor = disabledTrailingIconColor,
+        )
+
+    /**
+     * Default padding used for [DropdownMenuItem].
+     */
+    val DropdownMenuItemContentPadding = PaddingValues(
+        horizontal = DropdownMenuItemHorizontalPadding,
+        vertical = 0.dp
+    )
+}
+
+/**
+ * Represents the text and icon colors used in a menu item at different states.
+ *
+ * - See [MenuDefaults.itemColors] for the default colors used in a [DropdownMenuItemContent].
+ */
+@Stable
+interface MenuItemColors {
+
+    /**
+     * Represents the text color for a menu item, depending on its [enabled] state.
+     *
+     * @param enabled whether the menu item is enabled
+     */
+    @Composable
+    fun textColor(enabled: Boolean): State<Color>
+
+    /**
+     * Represents the leading icon color for a menu item, depending on its [enabled] state.
+     *
+     * @param enabled whether the menu item is enabled
+     */
+    @Composable
+    fun leadingIconColor(enabled: Boolean): State<Color>
+
+    /**
+     * Represents the trailing icon color for a menu item, depending on its [enabled] state.
+     *
+     * @param enabled whether the menu item is enabled
+     */
+    @Composable
+    fun trailingIconColor(enabled: Boolean): State<Color>
+}
+
+internal fun calculateTransformOrigin(
+    parentBounds: IntRect,
+    menuBounds: IntRect
+): TransformOrigin {
+    val pivotX = when {
+        menuBounds.left >= parentBounds.right -> 0f
+        menuBounds.right <= parentBounds.left -> 1f
+        menuBounds.width == 0 -> 0f
+        else -> {
+            val intersectionCenter =
+                (
+                    max(parentBounds.left, menuBounds.left) +
+                        min(parentBounds.right, menuBounds.right)
+                    ) / 2
+            (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
+        }
+    }
+    val pivotY = when {
+        menuBounds.top >= parentBounds.bottom -> 0f
+        menuBounds.bottom <= parentBounds.top -> 1f
+        menuBounds.height == 0 -> 0f
+        else -> {
+            val intersectionCenter =
+                (
+                    max(parentBounds.top, menuBounds.top) +
+                        min(parentBounds.bottom, menuBounds.bottom)
+                    ) / 2
+            (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
+        }
+    }
+    return TransformOrigin(pivotX, pivotY)
+}
+
+// Menu positioning.
+
+/**
+ * Calculates the position of a Material [DropdownMenu].
+ */
+// TODO(popam): Investigate if this can/should consider the app window size rather than screen size
+@Immutable
+internal data class DropdownMenuPositionProvider(
+    val contentOffset: DpOffset,
+    val density: Density,
+    val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        // The min margin above and below the menu, relative to the screen.
+        val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
+        // The content offset specified using the dropdown offset parameter.
+        val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
+        val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
+
+        // Compute horizontal position.
+        val toRight = anchorBounds.left + contentOffsetX
+        val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
+        val toDisplayRight = windowSize.width - popupContentSize.width
+        val toDisplayLeft = 0
+        val x = if (layoutDirection == LayoutDirection.Ltr) {
+            sequenceOf(
+                toRight,
+                toLeft,
+                // If the anchor gets outside of the window on the left, we want to position
+                // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
+                if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft
+            )
+        } else {
+            sequenceOf(
+                toLeft,
+                toRight,
+                // If the anchor gets outside of the window on the right, we want to position
+                // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
+                if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight
+            )
+        }.firstOrNull {
+            it >= 0 && it + popupContentSize.width <= windowSize.width
+        } ?: toLeft
+
+        // Compute vertical position.
+        val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
+        val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
+        val toCenter = anchorBounds.top - popupContentSize.height / 2
+        val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
+        val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
+            it >= verticalMargin &&
+                it + popupContentSize.height <= windowSize.height - verticalMargin
+        } ?: toTop
+
+        onPositionCalculated(
+            anchorBounds,
+            IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
+        )
+        return IntOffset(x, y)
+    }
+}
+
+/** Default [MenuItemColors] implementation. */
+@Immutable
+private class DefaultMenuItemColors(
+    private val textColor: Color,
+    private val leadingIconColor: Color,
+    private val trailingIconColor: Color,
+    private val disabledTextColor: Color,
+    private val disabledLeadingIconColor: Color,
+    private val disabledTrailingIconColor: Color,
+) : MenuItemColors {
+
+    @Composable
+    override fun textColor(enabled: Boolean): State<Color> {
+        return rememberUpdatedState(if (enabled) textColor else disabledTextColor)
+    }
+
+    @Composable
+    override fun leadingIconColor(enabled: Boolean): State<Color> {
+        return rememberUpdatedState(if (enabled) leadingIconColor else disabledLeadingIconColor)
+    }
+
+    @Composable
+    override fun trailingIconColor(enabled: Boolean): State<Color> {
+        return rememberUpdatedState(if (enabled) trailingIconColor else disabledTrailingIconColor)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as DefaultMenuItemColors
+
+        if (textColor != other.textColor) return false
+        if (leadingIconColor != other.leadingIconColor) return false
+        if (trailingIconColor != other.trailingIconColor) return false
+        if (disabledTextColor != other.disabledTextColor) return false
+        if (disabledLeadingIconColor != other.disabledLeadingIconColor) return false
+        if (disabledTrailingIconColor != other.disabledTrailingIconColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = textColor.hashCode()
+        result = 31 * result + leadingIconColor.hashCode()
+        result = 31 * result + trailingIconColor.hashCode()
+        result = 31 * result + disabledTextColor.hashCode()
+        result = 31 * result + disabledLeadingIconColor.hashCode()
+        result = 31 * result + disabledTrailingIconColor.hashCode()
+        return result
+    }
+}
+
+// Size defaults.
+internal val MenuVerticalMargin = 48.dp
+private val DropdownMenuItemHorizontalPadding = 12.dp
+internal val DropdownMenuVerticalPadding = 8.dp
+private val DropdownMenuItemDefaultMinWidth = 112.dp
+private val DropdownMenuItemDefaultMaxWidth = 280.dp
+
+// Menu open/close animation.
+internal const val InTransitionDuration = 120
+internal const val OutTransitionDuration = 75
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
index 5062093..86623af 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
@@ -207,6 +207,10 @@
  */
 @Composable
 @NonRestartableComposable
+@Deprecated(
+    message = "Please use Surface with an InteractionSource and a Modifier.clickable() instead.",
+    level = DeprecationLevel.ERROR
+)
 fun Surface(
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt
new file mode 100644
index 0000000..4602731
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 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.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object MenuTokens {
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level2
+    val ContainerShape = ShapeTokens.Small
+    val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
+    val DividerHeight = 1.0.dp
+    val ListItemContainerHeight = 48.0.dp
+    val ListItemDisabledLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    const val ListItemDisabledLabelTextOpacity = 0.38f
+    val ListItemFocusLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemFocusStateLayerColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemHoverLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemHoverStateLayerColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemLabelTextFont = TypographyKeyTokens.LabelLarge
+    val ListItemPressedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemPressedStateLayerColor = ColorSchemeKeyTokens.OnSurface
+    val ListItemSelectedContainerColor = ColorSchemeKeyTokens.SurfaceVariant
+    val ListItemDisabledLeadingIconColor = ColorSchemeKeyTokens.OnSurface
+    const val ListItemDisabledLeadingIconOpacity = 0.38f
+    val ListItemLeadingFocusIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemLeadingHoverIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemLeadingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemLeadingIconSize = 24.0.dp
+    val ListItemLeadingPressedIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemDisabledTrailingIconColor = ColorSchemeKeyTokens.OnSurface
+    const val ListItemDisabledTrailingIconOpacity = 0.38f
+    val ListItemTrailingFocusIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemTrailingHoverIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemTrailingPressedIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemTrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val ListItemTrailingIconSize = 24.0.dp
+}
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
index 033b693..1d15b6c 100644
--- a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
+++ b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
@@ -410,6 +410,44 @@
             assertThat(composedValue).isEqualTo(1)
         }
     }
+
+    @Test
+    fun changingInputIsNotAffectingOrderOfRestoration() {
+        var counter = 0
+        var input by mutableStateOf(0)
+        var withInput: Int? = null
+        var withoutInput: String? = null
+
+        restorationTester.setContent {
+            withInput = rememberSaveable(input) { counter++ }
+            withoutInput = rememberSaveable { (counter++).toString() }
+        }
+
+        rule.runOnIdle {
+            assertThat(withInput).isNotNull()
+            withInput = null
+            input++
+        }
+
+        var expectedWithInput: Int? = null
+        var expectedWithoutInput: String? = null
+
+        rule.runOnIdle {
+            assertThat(withInput).isNotNull()
+            assertThat(withoutInput).isNotNull()
+            expectedWithInput = withInput
+            expectedWithoutInput = withoutInput
+            withInput = null
+            withoutInput = null
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(withInput).isEqualTo(expectedWithInput)
+            assertThat(withoutInput).isEqualTo(expectedWithoutInput)
+        }
+    }
 }
 
 @Composable
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
index 44c0b1f..68f4cee 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.neverEqualPolicy
 import androidx.compose.runtime.referentialEqualityPolicy
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshots.SnapshotMutableState
 import androidx.compose.runtime.structuralEqualityPolicy
 
@@ -87,17 +88,19 @@
         restored ?: init()
     }
 
-    // save the latest passed saver object into a state object to be able to use it when we will
-    // be saving the value. keeping value in mutableStateOf() allows us to properly handle
-    // possible compose transactions cancellations
-    val saverHolder = remember { mutableStateOf(saver) }
-    saverHolder.value = saver
-
     // re-register if the registry or key has been changed
     if (registry != null) {
-        DisposableEffect(registry, finalKey, value) {
+        // we want to use the latest instances of saver and value in the valueProvider lambda
+        // without restarting DisposableEffect as it would cause re-registering the provider in
+        // the different order. so we use rememberUpdatedState.
+        val saverState = rememberUpdatedState(saver)
+        val valueState = rememberUpdatedState(value)
+
+        DisposableEffect(registry, finalKey) {
             val valueProvider = {
-                with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) }
+                with(saverState.value) {
+                    SaverScope { registry.canBeSaved(it) }.save(valueState.value)
+                }
             }
             registry.requireCanBeSaved(valueProvider())
             val entry = registry.registerProvider(finalKey, valueProvider)
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 9e5c9ce..6249ecb 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -922,7 +922,7 @@
 
   public interface PlatformTextInputService {
     method public void hideSoftwareKeyboard();
-    method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public void showSoftwareKeyboard();
     method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
     method public void stopInput();
@@ -999,7 +999,7 @@
     method public void dispose();
     method public boolean hideSoftwareKeyboard();
     method public boolean isOpen();
-    method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public boolean showSoftwareKeyboard();
     method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
     property public final boolean isOpen;
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 49abfc4..71868a4 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -960,7 +960,7 @@
 
   public interface PlatformTextInputService {
     method public void hideSoftwareKeyboard();
-    method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public void showSoftwareKeyboard();
     method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
     method public void stopInput();
@@ -1037,7 +1037,7 @@
     method public void dispose();
     method public boolean hideSoftwareKeyboard();
     method public boolean isOpen();
-    method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public boolean showSoftwareKeyboard();
     method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
     property public final boolean isOpen;
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 9e5c9ce..6249ecb 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -922,7 +922,7 @@
 
   public interface PlatformTextInputService {
     method public void hideSoftwareKeyboard();
-    method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public void showSoftwareKeyboard();
     method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
     method public void stopInput();
@@ -999,7 +999,7 @@
     method public void dispose();
     method public boolean hideSoftwareKeyboard();
     method public boolean isOpen();
-    method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+    method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
     method public boolean showSoftwareKeyboard();
     method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
     property public final boolean isOpen;
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index e145592..a97d3cd 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -147,17 +147,9 @@
         }
     }
 
-    /**
-     * Notify the focused rectangle to the system.
-     *
-     * If the session is not open, no action will be performed.
-     *
-     * @param rect the rectangle that describes the boundaries on the screen that requires focus
-     * @return false if this session expired and no action was performed
-     */
-    fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession {
-        platformTextInputService.notifyFocusedRect(rect)
-    }
+    @Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER")
+    @Deprecated("This method is not called, used BringIntoViewRequester instead.")
+    fun notifyFocusedRect(rect: Rect): Boolean = false
 
     /**
      * Notify IME about the new [TextFieldValue] and latest state of the editing buffer. [oldValue]
@@ -261,10 +253,7 @@
      */
     fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue)
 
-    /**
-     * Notify the focused rectangle to the system.
-     *
-     * @see TextInputSession.notifyFocusedRect
-     */
-    fun notifyFocusedRect(rect: Rect)
+    @Deprecated("This method is not called, used BringIntoViewRequester instead.")
+    fun notifyFocusedRect(rect: Rect) {
+    }
 }
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
index 92c1bb1..c904ccd 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
@@ -16,9 +16,6 @@
 
 package androidx.compose.ui.text
 
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.PlatformTextInputService
 import androidx.compose.ui.text.input.TextFieldValue
@@ -238,51 +235,4 @@
         secondSession.updateState(null, editorModel)
         verify(platformService).updateState(eq(null), eq(editorModel))
     }
-
-    @Test
-    fun notifyFocusedRect_with_valid_token() {
-        val platformService = mock<PlatformTextInputService>()
-
-        val textInputService = TextInputService(platformService)
-
-        val firstSession = textInputService.startInput(
-            TextFieldValue(),
-            ImeOptions.Default,
-            {}, // onEditCommand
-            {} // onImeActionPerformed
-        )
-
-        val rect = Rect(Offset.Zero, Size(100f, 100f))
-        firstSession.notifyFocusedRect(rect)
-        verify(platformService, times(1)).notifyFocusedRect(eq(rect))
-    }
-
-    @Test
-    fun notifyFocusedRect_with_expired_token() {
-        val platformService = mock<PlatformTextInputService>()
-
-        val textInputService = TextInputService(platformService)
-
-        val firstSession = textInputService.startInput(
-            TextFieldValue(),
-            ImeOptions.Default,
-            {}, // onEditCommand
-            {} // onImeActionPerformed
-        )
-
-        // Start another session. The firstToken is now expired.
-        val secondSession = textInputService.startInput(
-            TextFieldValue(),
-            ImeOptions.Default,
-            {}, // onEditCommand
-            {} // onImeActionPerformed
-        )
-
-        val rect = Rect(Offset.Zero, Size(100f, 100f))
-        firstSession.notifyFocusedRect(rect)
-        verify(platformService, never()).notifyFocusedRect(any())
-
-        secondSession.notifyFocusedRect(rect)
-        verify(platformService, times(1)).notifyFocusedRect(eq(rect))
-    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
index 9e2ccd6..91fdc26 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
@@ -89,7 +89,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Next)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Next)
+            }
         } else {
             rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
         }
@@ -136,7 +138,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Previous)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Previous)
+            }
         } else {
             val nativeEvent = AndroidKeyEvent(0L, 0L, KeyDown, Tab.nativeKeyCode, 0, META_SHIFT_ON)
             rule.onRoot().performKeyPress(KeyEvent(nativeEvent))
@@ -184,7 +188,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Up)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Up)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionUp.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -232,7 +238,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Down)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Down)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionDown.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -280,7 +288,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Left)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Left)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionLeft.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -328,7 +338,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Right)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Right)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionRight.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -378,7 +390,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Left)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Left)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionLeft.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -428,7 +442,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Right)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Right)
+            }
         } else {
             val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionRight.nativeKeyCode)
             rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -485,7 +501,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Next)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Next)
+            }
         } else {
             rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
         }
@@ -535,7 +553,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Next)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Next)
+            }
         } else {
             rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
         }
@@ -584,7 +604,9 @@
 
         // Act.
         if (moveFocusProgrammatically) {
-            focusManager.moveFocus(FocusDirection.Next)
+            rule.runOnIdle {
+                focusManager.moveFocus(FocusDirection.Next)
+            }
         } else {
             rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
         }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
new file mode 100644
index 0000000..47e1944
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 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.focus
+
+import android.graphics.Rect as AndroidRect
+import android.view.View
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.dp
+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 FocusViewInteropTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun getFocusedRect_reportsFocusBounds_whenFocused() {
+        val focusRequester = FocusRequester()
+        var hasFocus = false
+        lateinit var view: View
+        rule.setContent {
+            view = LocalView.current
+            CompositionLocalProvider(LocalDensity provides Density(density = 1f)) {
+                Box(
+                    Modifier
+                        .size(90.dp, 100.dp)
+                        .wrapContentSize(align = Alignment.TopStart)
+                        .size(10.dp, 20.dp)
+                        .offset(30.dp, 40.dp)
+                        .onFocusChanged {
+                            if (it.isFocused) {
+                                hasFocus = true
+                            }
+                        }
+                        .focusRequester(focusRequester)
+                        .focusable()
+                )
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitUntil { hasFocus }
+
+        assertThat(view.getFocusedRect()).isEqualTo(IntRect(30, 40, 40, 60))
+    }
+
+    @Test
+    fun getFocusedRect_reportsEntireView_whenNoFocus() {
+        lateinit var view: View
+        rule.setContent {
+            view = LocalView.current
+            CompositionLocalProvider(LocalDensity provides Density(density = 1f)) {
+                Box(
+                    Modifier
+                        .size(90.dp, 100.dp)
+                        .wrapContentSize(align = Alignment.TopStart)
+                        .size(10.dp, 20.dp)
+                        .offset(30.dp, 40.dp)
+                        .focusable()
+                )
+            }
+        }
+
+        assertThat(view.getFocusedRect()).isEqualTo(
+            IntRect(0, 0, 90, 100)
+        )
+    }
+
+    private fun View.getFocusedRect() = AndroidRect().run {
+        rule.runOnIdle {
+            getFocusedRect(this)
+        }
+        IntRect(left, top, right, bottom)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
index e74a88a..414f89d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
@@ -115,7 +115,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -184,7 +186,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -258,7 +262,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -332,7 +338,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -402,7 +410,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -472,7 +482,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -542,7 +554,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -612,7 +626,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -682,7 +698,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -752,7 +770,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -821,7 +841,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -891,7 +913,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -961,7 +985,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1031,7 +1057,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1101,7 +1129,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1170,7 +1200,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1240,7 +1272,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1310,7 +1344,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1381,7 +1417,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1451,7 +1489,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1521,7 +1561,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1592,7 +1634,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1660,7 +1704,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1729,7 +1775,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1798,7 +1846,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1867,7 +1917,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1934,7 +1986,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2001,7 +2055,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2070,7 +2126,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2140,7 +2198,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2210,7 +2270,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2279,7 +2341,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2344,7 +2408,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2412,7 +2478,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2477,7 +2545,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2544,7 +2614,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2613,7 +2685,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2683,7 +2757,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2753,7 +2829,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2821,7 +2899,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2885,7 +2965,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2948,7 +3030,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3011,7 +3095,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3076,7 +3162,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3145,7 +3233,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3212,7 +3302,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3281,7 +3373,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3348,7 +3442,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3417,7 +3513,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3484,7 +3582,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3554,7 +3654,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3632,7 +3734,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3711,7 +3815,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3792,7 +3898,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3872,7 +3980,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3950,7 +4060,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4031,7 +4143,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4111,7 +4225,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4189,7 +4305,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4260,7 +4378,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4329,7 +4449,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4398,7 +4520,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4468,7 +4592,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4538,7 +4664,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4607,7 +4735,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4676,7 +4806,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4745,7 +4877,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4808,7 +4942,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4881,7 +5017,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4954,7 +5092,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -5027,7 +5167,9 @@
             }
         }
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
index 00eed69..52425ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
@@ -77,7 +77,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -113,7 +115,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -149,7 +153,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -185,7 +191,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -221,7 +229,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -257,7 +267,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -293,7 +305,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -329,7 +343,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -364,7 +380,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -398,7 +416,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -432,7 +452,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -467,7 +489,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -502,7 +526,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -537,7 +563,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -572,7 +600,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -605,7 +635,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -639,7 +671,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -673,7 +707,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -708,7 +744,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -744,7 +782,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -780,7 +820,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -816,7 +858,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -852,7 +896,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -888,7 +934,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -924,7 +972,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -960,7 +1010,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -996,7 +1048,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1032,7 +1086,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1068,7 +1124,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1104,7 +1162,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1140,7 +1200,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1175,7 +1237,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1209,7 +1273,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1243,7 +1309,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1276,7 +1344,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1311,7 +1381,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1346,7 +1418,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1381,7 +1455,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1416,7 +1492,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1450,7 +1528,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1484,7 +1564,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1519,7 +1601,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1555,7 +1639,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1591,7 +1677,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1627,7 +1715,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1663,7 +1753,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1698,7 +1790,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1733,7 +1827,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1768,7 +1864,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1803,7 +1901,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1838,7 +1938,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1873,7 +1975,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1908,7 +2012,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1942,7 +2048,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -1976,7 +2084,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2011,7 +2121,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2046,7 +2158,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2081,7 +2195,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2116,7 +2232,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2149,7 +2267,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2183,7 +2303,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2217,7 +2339,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2252,7 +2376,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2287,7 +2413,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2322,7 +2450,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2357,7 +2487,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2392,7 +2524,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2427,7 +2561,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2462,7 +2598,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2497,7 +2635,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2532,7 +2672,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2567,7 +2709,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2601,7 +2745,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2635,7 +2781,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2668,7 +2816,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2703,7 +2853,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2738,7 +2890,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2773,7 +2927,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2808,7 +2964,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2842,7 +3000,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2876,7 +3036,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2911,7 +3073,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2946,7 +3110,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -2981,7 +3147,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3017,7 +3185,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3053,7 +3223,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3089,7 +3261,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3125,7 +3299,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3161,7 +3337,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3195,7 +3373,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3229,7 +3409,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3264,7 +3446,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3299,7 +3483,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3334,7 +3520,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3369,7 +3557,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3402,7 +3592,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3436,7 +3628,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3470,7 +3664,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3506,7 +3702,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3542,7 +3740,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3578,7 +3778,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3614,7 +3816,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3650,7 +3854,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3686,7 +3892,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3720,7 +3928,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3754,7 +3964,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3787,7 +3999,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3822,7 +4036,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3857,7 +4073,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3892,7 +4110,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3927,7 +4147,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3961,7 +4183,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -3995,7 +4219,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4031,7 +4257,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4066,7 +4294,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4092,7 +4322,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4118,7 +4350,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4144,7 +4378,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4170,7 +4406,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4196,7 +4434,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4222,7 +4462,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4248,7 +4490,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4275,7 +4519,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4301,7 +4547,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4327,7 +4575,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4353,7 +4603,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4379,7 +4631,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4405,7 +4659,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4431,7 +4687,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4457,7 +4715,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4483,7 +4743,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4510,7 +4772,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4575,7 +4839,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -4595,7 +4861,9 @@
         }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle {
+            focusManager.moveFocus(focusDirection)
+        }
 
         // Assert.
         rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 8637ce5..dc297f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -35,6 +35,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
@@ -55,6 +57,7 @@
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Constraints
@@ -67,14 +70,14 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertThrows
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -1756,6 +1759,44 @@
         }
     }
 
+    @Test
+    fun stateIsRestoredWhenGoBackToScreen1WithSubcomposition() {
+        val restorationTester = StateRestorationTester(rule)
+
+        var increment = 0
+        var screen by mutableStateOf(Screens.Screen1)
+        var restorableNumberOnScreen1 = -1
+        restorationTester.setContent {
+            val holder = rememberSaveableStateHolder()
+            holder.SaveableStateProvider(screen) {
+                if (screen == Screens.Screen1) {
+                    SubcomposeLayout {
+                        subcompose(Unit) {
+                            restorableNumberOnScreen1 = rememberSaveable { increment++ }
+                        }
+                        layout(10, 10) {}
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(restorableNumberOnScreen1).isEqualTo(0)
+            screen = Screens.Screen2
+        }
+
+        // wait for the screen switch to apply
+        rule.runOnIdle {
+            restorableNumberOnScreen1 = -1
+            // switch back to screen1
+            screen = Screens.Screen1
+        }
+
+        rule.runOnIdle {
+            assertThat(restorableNumberOnScreen1).isEqualTo(0)
+        }
+    }
+
     private fun composeItems(
         state: SubcomposeLayoutState,
         items: MutableState<List<Int>>
@@ -1804,4 +1845,9 @@
             placeable.place(0, 0)
         }
     }
+}
+
+private enum class Screens {
+    Screen1,
+    Screen2,
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index d8bcdc2..b68ed8e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.platform
 
+import android.view.KeyEvent as AndroidKeyEvent
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.Configuration
@@ -113,6 +114,7 @@
 import androidx.compose.ui.input.rotary.RotaryScrollEvent
 import androidx.compose.ui.input.rotary.onRotaryScrollEvent
 import androidx.compose.ui.layout.RootMeasurePolicy
+import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.UsageByParent
@@ -153,7 +155,7 @@
 import androidx.savedstate.SavedStateRegistryOwner
 import androidx.savedstate.ViewTreeSavedStateRegistryOwner
 import java.lang.reflect.Method
-import android.view.KeyEvent as AndroidKeyEvent
+import kotlin.math.roundToInt
 
 @SuppressLint("ViewConstructor", "VisibleForTests")
 @OptIn(ExperimentalComposeUiApi::class)
@@ -525,6 +527,19 @@
         }
     }
 
+    /**
+     * Since this view has its own concept of internal focus, it needs to report that to the view
+     * system for accurate focus searching and so ViewRootImpl will scroll correctly.
+     */
+    override fun getFocusedRect(rect: Rect) {
+        _focusManager.getActiveFocusModifier()?.focusNode?.boundsInRoot()?.let {
+            rect.left = it.left.roundToInt()
+            rect.top = it.top.roundToInt()
+            rect.right = it.right.roundToInt()
+            rect.bottom = it.bottom.roundToInt()
+        } ?: super.getFocusedRect(rect)
+    }
+
     override fun onResume(owner: LifecycleOwner) {
         // Refresh in onResume in case the value has changed.
         @OptIn(InternalCoreApi::class)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 6a5dc7c..39afec1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -20,18 +20,15 @@
 import android.util.Log
 import android.view.KeyEvent
 import android.view.View
-import android.view.ViewTreeObserver
 import android.view.inputmethod.BaseInputConnection
 import android.view.inputmethod.EditorInfo
 import android.view.inputmethod.InputConnection
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.HideKeyboard
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.ShowKeyboard
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
 import androidx.core.view.inputmethod.EditorInfoCompat
-import kotlin.math.roundToInt
 import kotlinx.coroutines.channels.Channel
 
 private const val DEBUG_CLASS = "TextInputServiceAndroid"
@@ -78,8 +75,6 @@
         BaseInputConnection(view, false)
     }
 
-    private var focusedRect: android.graphics.Rect? = null
-
     /**
      * A channel that is used to debounce rapid operations such as showing/hiding the keyboard and
      * starting/stopping input, so we can make the minimal number of calls on the
@@ -88,30 +83,10 @@
      */
     private val textInputCommandChannel = Channel<TextInputCommand>(Channel.UNLIMITED)
 
-    private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
-        // focusedRect is null if there is not ongoing text input session. So safe to request
-        // latest focused rectangle whenever global layout has changed.
-        focusedRect?.let {
-            // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
-            // create another Rect and then pass it.
-            view.requestRectangleOnScreen(android.graphics.Rect(it))
-        }
-    }
-
     internal constructor(view: View) : this(view, InputMethodManagerImpl(view.context))
 
     init {
         if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.create") }
-
-        view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewDetachedFromWindow(v: View?) {
-                v?.rootView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
-            }
-
-            override fun onViewAttachedToWindow(v: View?) {
-                v?.rootView?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
-            }
-        })
     }
 
     /**
@@ -178,7 +153,6 @@
         editorHasFocus = false
         onEditCommand = {}
         onImeActionPerformed = {}
-        focusedRect = null
 
         // Don't actually send the command to the IME yet, it may be overruled by a subsequent call
         // to startInput.
@@ -353,29 +327,6 @@
         }
     }
 
-    override fun notifyFocusedRect(rect: Rect) {
-        focusedRect = android.graphics.Rect(
-            rect.left.roundToInt(),
-            rect.top.roundToInt(),
-            rect.right.roundToInt(),
-            rect.bottom.roundToInt()
-        )
-
-        // Requesting rectangle too early after obtaining focus may bring view into wrong place
-        // probably due to transient IME inset change. We don't know the correct timing of calling
-        // requestRectangleOnScreen API, so try to call this API only after the IME is ready to
-        // use, i.e. InputConnection has created.
-        // Even if we miss all the timing of requesting rectangle during initial text field focus,
-        // focused rectangle will be requested when software keyboard has shown.
-        if (ic == null) {
-            focusedRect?.let {
-                // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
-                // create another Rect and then pass it.
-                view.requestRectangleOnScreen(android.graphics.Rect(it))
-            }
-        }
-    }
-
     /** Immediately restart the IME connection, bypassing the [textInputCommandChannel]. */
     private fun restartInputImmediately() {
         if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index dbd2034..d719cc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -21,10 +21,12 @@
 import androidx.compose.runtime.ComposeNode
 import androidx.compose.runtime.Composition
 import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
 import androidx.compose.ui.materialize
@@ -119,6 +121,12 @@
             state.forceRecomposeChildren()
         }
     }
+    val stateHolder = rememberUpdatedState(state)
+    DisposableEffect(Unit) {
+        onDispose {
+            stateHolder.value.disposeCurrentNodes()
+        }
+    }
 }
 
 /**
@@ -211,6 +219,8 @@
 
     internal fun forceRecomposeChildren() = state.forceRecomposeChildren()
 
+    internal fun disposeCurrentNodes() = state.disposeCurrentNodes()
+
     /**
      * Instance of this interface is returned by [precompose] function.
      */
@@ -421,6 +431,8 @@
                     reusableCount++
                 } else {
                     ignoreRemeasureRequests {
+                        val nodeState = nodeToNodeState.remove(root.foldedChildren[i])!!
+                        nodeState.composition?.dispose()
                         root.removeAt(i, 1)
                     }
                 }
@@ -486,12 +498,6 @@
         }
     }
 
-    private fun disposeNode(node: LayoutNode) {
-        val nodeState = nodeToNodeState.remove(node)!!
-        nodeState.composition?.dispose()
-        slotIdToNode.remove(nodeState.slotId)
-    }
-
     fun createMeasurePolicy(
         block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
     ): MeasurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
@@ -601,12 +607,6 @@
         ignoreRemeasureRequests {
             root.insertAt(index, node)
         }
-        node.onDetach = {
-            disposeNode(node)
-            node.onAttach = {
-                throw IllegalStateException("Disposed node shouldn't be reattached")
-            }
-        }
     }
 
     private fun move(from: Int, to: Int, count: Int = 1) {
@@ -618,6 +618,14 @@
     private inline fun ignoreRemeasureRequests(block: () -> Unit) =
         root.ignoreRemeasureRequests(block)
 
+    fun disposeCurrentNodes() {
+        nodeToNodeState.values.forEach {
+            it.composition?.dispose()
+        }
+        nodeToNodeState.clear()
+        slotIdToNode.clear()
+    }
+
     private class NodeState(
         var slotId: Any?,
         var content: @Composable () -> Unit,
diff --git a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
index 656d4b1..3e22243 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
@@ -79,7 +79,7 @@
  */
 @CheckResult
 public inline fun <X, Y> LiveData<X>.switchMap(
-    crossinline transform: (X) -> LiveData<Y>
+    crossinline transform: (X) -> LiveData<Y>?
 ): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
 
 /**
diff --git a/lifecycle/lifecycle-process/build.gradle b/lifecycle/lifecycle-process/build.gradle
index 3369989..b09c8bc 100644
--- a/lifecycle/lifecycle-process/build.gradle
+++ b/lifecycle/lifecycle-process/build.gradle
@@ -31,7 +31,7 @@
 
 dependencies {
     api(project(":lifecycle:lifecycle-runtime"))
-    api("androidx.startup:startup-runtime:1.0.0")
+    api("androidx.startup:startup-runtime:1.1.1")
     api("androidx.annotation:annotation:1.2.0")
 
     testImplementation(libs.junit)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
index 8e3a3fb..6059aec 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
@@ -140,6 +140,15 @@
 
     @Test
     @UiThreadTest
+    fun newliveData_withInitialGet() {
+        val handle = SavedStateHandle()
+        val ld: LiveData<String?> = handle.getLiveData("aa", "xx")
+        ld.assertValue("xx")
+        assertThat(handle.get<String?>("aa")).isEqualTo("xx")
+    }
+
+    @Test
+    @UiThreadTest
     fun newLiveData_existingValue_withInitial() {
         val handle = SavedStateHandle()
         handle["aa"] = "existing"
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
index 7824d94..61add75 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
@@ -159,6 +159,7 @@
             @Suppress("UNCHECKED_CAST")
             SavingStateLiveData(this, key, regular[key] as T)
         } else if (hasInitialValue) {
+            regular[key] = initialValue
             SavingStateLiveData(this, key, initialValue)
         } else {
             SavingStateLiveData(this, key)