Merge "Add TextArea demo and ExposedDropdownMenu to catalog" into androidx-main
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
index c10893f..87ce6c3 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
@@ -58,6 +58,7 @@
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
 import androidx.compose.material.Slider
+import androidx.compose.material.Switch
 import androidx.compose.material.Text
 import androidx.compose.material.TextField
 import androidx.compose.material.TopAppBar
@@ -403,14 +404,25 @@
             verticalAlignment = Alignment.CenterVertically
         ) {
             Row {
-                Row(modifier = Modifier.padding(4.dp)) {
-                    Checkbox(
+                Column {
+                    Switch(
                         animation.value,
                         onCheckedChange = {
                             animation.value = it
                         }
                     )
-                    Text("Animation")
+                    Row(
+                        modifier = Modifier.padding(4.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Checkbox(
+                            animation.value,
+                            onCheckedChange = {
+                                animation.value = it
+                            }
+                        )
+                        Text("Animation")
+                    }
                 }
 
                 Button(
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 9f9bbe2..26fd022 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -51,6 +51,10 @@
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
+  public final class HoverableKt {
+    method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
+  }
+
   public final class ImageKt {
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
@@ -300,6 +304,23 @@
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsFocusedAsState(androidx.compose.foundation.interaction.InteractionSource);
   }
 
+  public interface HoverInteraction extends androidx.compose.foundation.interaction.Interaction {
+  }
+
+  public static final class HoverInteraction.Enter implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Enter();
+  }
+
+  public static final class HoverInteraction.Exit implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Exit(androidx.compose.foundation.interaction.HoverInteraction.Enter enter);
+    method public androidx.compose.foundation.interaction.HoverInteraction.Enter getEnter();
+    property public final androidx.compose.foundation.interaction.HoverInteraction.Enter enter;
+  }
+
+  public final class HoverInteractionKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsHoveredAsState(androidx.compose.foundation.interaction.InteractionSource);
+  }
+
   public interface Interaction {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 0559738..56f8458 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -57,6 +57,10 @@
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
+  public final class HoverableKt {
+    method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
+  }
+
   public final class ImageKt {
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
@@ -320,6 +324,23 @@
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsFocusedAsState(androidx.compose.foundation.interaction.InteractionSource);
   }
 
+  public interface HoverInteraction extends androidx.compose.foundation.interaction.Interaction {
+  }
+
+  public static final class HoverInteraction.Enter implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Enter();
+  }
+
+  public static final class HoverInteraction.Exit implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Exit(androidx.compose.foundation.interaction.HoverInteraction.Enter enter);
+    method public androidx.compose.foundation.interaction.HoverInteraction.Enter getEnter();
+    property public final androidx.compose.foundation.interaction.HoverInteraction.Enter enter;
+  }
+
+  public final class HoverInteractionKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsHoveredAsState(androidx.compose.foundation.interaction.InteractionSource);
+  }
+
   public interface Interaction {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 9f9bbe2..26fd022 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -51,6 +51,10 @@
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
+  public final class HoverableKt {
+    method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
+  }
+
   public final class ImageKt {
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
@@ -300,6 +304,23 @@
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsFocusedAsState(androidx.compose.foundation.interaction.InteractionSource);
   }
 
+  public interface HoverInteraction extends androidx.compose.foundation.interaction.Interaction {
+  }
+
+  public static final class HoverInteraction.Enter implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Enter();
+  }
+
+  public static final class HoverInteraction.Exit implements androidx.compose.foundation.interaction.HoverInteraction {
+    ctor public HoverInteraction.Exit(androidx.compose.foundation.interaction.HoverInteraction.Enter enter);
+    method public androidx.compose.foundation.interaction.HoverInteraction.Enter getEnter();
+    property public final androidx.compose.foundation.interaction.HoverInteraction.Enter enter;
+  }
+
+  public final class HoverInteractionKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<java.lang.Boolean> collectIsHoveredAsState(androidx.compose.foundation.interaction.InteractionSource);
+  }
+
   public interface Interaction {
   }
 
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index f31c312..0318f41 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -123,6 +123,7 @@
                 implementation(project(":compose:test-utils"))
                 implementation(project(":compose:ui:ui-test-font"))
                 implementation(project(":test:screenshot:screenshot"))
+                implementation(project(":internal-testutils-runtime"))
                 implementation("androidx.activity:activity-compose:1.3.1")
 
                 implementation(libs.testUiautomator)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
index 8d3efe7..05ce920 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
@@ -19,17 +19,20 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.samples.DraggableSample
 import androidx.compose.foundation.samples.FocusableSample
+import androidx.compose.foundation.samples.HoverableSample
 import androidx.compose.foundation.samples.ScrollableSample
 import androidx.compose.foundation.samples.TransformableSample
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 
 @Composable
 fun HighLevelGesturesDemo() {
-    Column {
+    Column(Modifier.verticalScroll(rememberScrollState())) {
         DraggableSample()
         Spacer(Modifier.height(50.dp))
         ScrollableSample()
@@ -37,5 +40,7 @@
         TransformableSample()
         Spacer(Modifier.height(50.dp))
         FocusableSample()
+        Spacer(Modifier.height(50.dp))
+        HoverableSample()
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/HoverableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/HoverableSample.kt
new file mode 100644
index 0000000..411477d
--- /dev/null
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/HoverableSample.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun HoverableSample() {
+    // MutableInteractionSource to track changes of the component's interactions (like "hovered")
+    val interactionSource = remember { MutableInteractionSource() }
+    val isHovered by interactionSource.collectIsHoveredAsState()
+
+    // the color will change depending on the presence of a hover
+    Box(
+        modifier = Modifier
+            .size(128.dp)
+            .background(if (isHovered) Color.Red else Color.Blue)
+            .hoverable(interactionSource = interactionSource),
+        contentAlignment = Alignment.Center
+    ) {
+        Text(if (isHovered) "Hovered" else "Unhovered")
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 8a97de9..2f8b9d6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
@@ -42,6 +43,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertHasClickAction
@@ -58,6 +60,7 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
@@ -820,6 +823,111 @@
         }
     }
 
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun clickableTest_interactionSource_hover() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                BasicText(
+                    "ClickableText",
+                    modifier = Modifier
+                        .testTag("myClickable")
+                        .combinedClickable(
+                            interactionSource = interactionSource,
+                            indication = null
+                        ) {}
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag("myClickable")
+            .performMouseInput { enter(center) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        rule.onNodeWithTag("myClickable")
+            .performMouseInput { exit(Offset(-1f, -1f)) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun clickableTest_interactionSource_hover_and_press() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                BasicText(
+                    "ClickableText",
+                    modifier = Modifier
+                        .testTag("myClickable")
+                        .combinedClickable(
+                            interactionSource = interactionSource,
+                            indication = null
+                        ) {}
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag("myClickable")
+            .performMouseInput {
+                enter(center)
+                click()
+                exit(Offset(-1f, -1f))
+            }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(4)
+            assertThat(interactions[0]).isInstanceOf(HoverInteraction.Enter::class.java)
+            assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+            assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
+            assertThat(interactions[3]).isInstanceOf(HoverInteraction.Exit::class.java)
+            assertThat((interactions[2] as PressInteraction.Release).press)
+                .isEqualTo(interactions[1])
+            assertThat((interactions[3] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
     /**
      * Regression test for b/186223077
      *
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/HoverableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/HoverableTest.kt
new file mode 100644
index 0000000..634b442
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/HoverableTest.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+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 HoverableTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val hoverTag = "myHoverable"
+
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun hoverableText_testInspectorValue() {
+        rule.setContent {
+            val interactionSource = remember { MutableInteractionSource() }
+            val modifier = Modifier.hoverable(interactionSource) as InspectableValue
+            Truth.assertThat(modifier.nameFallback).isEqualTo("hoverable")
+            Truth.assertThat(modifier.valueOverride).isNull()
+            Truth.assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly(
+                    "interactionSource",
+                    "enabled",
+                )
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @ExperimentalComposeUiApi
+    @Test
+    fun hoverableTest_hovered() {
+        var isHovered = false
+        val interactionSource = MutableInteractionSource()
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .size(128.dp)
+                    .testTag(hoverTag)
+                    .hoverable(interactionSource)
+            )
+
+            isHovered = interactionSource.collectIsHoveredAsState().value
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            enter(Offset(64.dp.toPx(), 64.dp.toPx()))
+        }
+
+        rule.waitForIdle()
+        Truth.assertThat(isHovered).isTrue()
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            moveTo(Offset(96.dp.toPx(), 96.dp.toPx()))
+        }
+
+        rule.waitForIdle()
+        Truth.assertThat(isHovered).isTrue()
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            moveTo(Offset(129.dp.toPx(), 129.dp.toPx()))
+        }
+
+        rule.waitForIdle()
+        Truth.assertThat(isHovered).isFalse()
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            moveTo(Offset(96.dp.toPx(), 96.dp.toPx()))
+        }
+
+        rule.waitForIdle()
+        Truth.assertThat(isHovered).isTrue()
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @ExperimentalComposeUiApi
+    @Test
+    fun hoverableTest_interactionSource() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box(
+                modifier = Modifier
+                    .size(128.dp)
+                    .testTag(hoverTag)
+                    .hoverable(interactionSource = interactionSource)
+            )
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            enter(Offset(64.dp.toPx(), 64.dp.toPx()))
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(1)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            moveTo(Offset(129.dp.toPx(), 129.dp.toPx()))
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(2)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            Truth.assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            Truth.assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun hoverableTest_interactionSource_resetWhenDisposed() {
+        val interactionSource = MutableInteractionSource()
+        var emitHoverable by mutableStateOf(true)
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                if (emitHoverable) {
+                    Box(
+                        modifier = Modifier
+                            .size(128.dp)
+                            .testTag(hoverTag)
+                            .hoverable(interactionSource = interactionSource)
+                    )
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            enter(Offset(64.dp.toPx(), 64.dp.toPx()))
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(1)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        // Dispose hoverable, Interaction should be gone
+        rule.runOnIdle {
+            emitHoverable = false
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(2)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            Truth.assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            Truth.assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun hoverableTest_interactionSource_dontHoverWhenDisabled() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    modifier = Modifier
+                        .size(128.dp)
+                        .testTag(hoverTag)
+                        .hoverable(interactionSource = interactionSource, enabled = false)
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            enter(Offset(64.dp.toPx(), 64.dp.toPx()))
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun hoverableTest_interactionSource_resetWhenDisabled() {
+        val interactionSource = MutableInteractionSource()
+        var enableHoverable by mutableStateOf(true)
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    modifier = Modifier
+                        .size(128.dp)
+                        .testTag(hoverTag)
+                        .hoverable(interactionSource = interactionSource, enabled = enableHoverable)
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag(hoverTag).performMouseInput {
+            enter(Offset(64.dp.toPx(), 64.dp.toPx()))
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(1)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        // Disable hoverable, Interaction should be gone
+        rule.runOnIdle {
+            enableHoverable = false
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(2)
+            Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            Truth.assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            Truth.assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
index a18359c..b74d3a8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
@@ -29,10 +30,12 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.first
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertCountEquals
@@ -44,6 +47,7 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -293,6 +297,60 @@
         }
     }
 
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun selectableTest_interactionSource_hover() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    Modifier.selectable(
+                        selected = true,
+                        interactionSource = interactionSource,
+                        indication = null,
+                        onClick = {}
+                    )
+                ) {
+                    BasicText("SelectableText")
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithText("SelectableText")
+            .performMouseInput { enter(center) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        rule.onNodeWithText("SelectableText")
+            .performMouseInput { exit(Offset(-1f, -1f)) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
     @Test
     fun selectableTest_testInspectorValue_noIndication() {
         rule.setContent {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
index a50acae..3de6204 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
@@ -39,6 +40,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.state.ToggleableState
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertHasClickAction
@@ -56,6 +58,7 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -389,6 +392,60 @@
         }
     }
 
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun toggleableTest_interactionSource_hover() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    Modifier.toggleable(
+                        value = true,
+                        interactionSource = interactionSource,
+                        indication = null,
+                        onValueChange = {}
+                    )
+                ) {
+                    BasicText("ToggleableText")
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithText("ToggleableText")
+            .performMouseInput { enter(center) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+        }
+
+        rule.onNodeWithText("ToggleableText")
+            .performMouseInput { exit(Offset(-1f, -1f)) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(HoverInteraction.Exit::class.java)
+            assertThat((interactions[1] as HoverInteraction.Exit).enter)
+                .isEqualTo(interactions[0])
+        }
+    }
+
     @Test
     fun toggleableText_testInspectorValue_noIndication() {
         rule.setContent {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 834b038..e752677 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -426,5 +426,6 @@
     return this
         .then(semanticModifier)
         .indication(interactionSource, indication)
+        .hoverable(interactionSource = interactionSource)
         .then(gestureModifiers)
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Hoverable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Hoverable.kt
new file mode 100644
index 0000000..788671a
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Hoverable.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+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.composed
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.debugInspectorInfo
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
+
+/**
+ * Configure component to be hoverable via pointer enter/exit events.
+ *
+ * @sample androidx.compose.foundation.samples.HoverableSample
+ *
+ * @param interactionSource [MutableInteractionSource] that will be used to emit
+ * [HoverInteraction.Enter] when this element is being hovered.
+ * @param enabled Controls the enabled state. When `false`, hover events will be ignored.
+ */
+fun Modifier.hoverable(
+    interactionSource: MutableInteractionSource,
+    enabled: Boolean = true
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "hoverable"
+        properties["interactionSource"] = interactionSource
+        properties["enabled"] = enabled
+    }
+) {
+    var hoverInteraction by remember { mutableStateOf<HoverInteraction.Enter?>(null) }
+
+    suspend fun emitEnter() {
+        if (hoverInteraction == null) {
+            val interaction = HoverInteraction.Enter()
+            interactionSource.emit(interaction)
+            hoverInteraction = interaction
+        }
+    }
+
+    suspend fun emitExit() {
+        hoverInteraction?.let { oldValue ->
+            val interaction = HoverInteraction.Exit(oldValue)
+            interactionSource.emit(interaction)
+            hoverInteraction = null
+        }
+    }
+
+    fun tryEmitExit() {
+        hoverInteraction?.let { oldValue ->
+            val interaction = HoverInteraction.Exit(oldValue)
+            interactionSource.tryEmit(interaction)
+            hoverInteraction = null
+        }
+    }
+
+    DisposableEffect(interactionSource) {
+        onDispose { tryEmitExit() }
+    }
+    LaunchedEffect(enabled) {
+        if (!enabled) {
+            emitExit()
+        }
+    }
+
+    if (enabled) {
+        Modifier
+// TODO(b/202505231):
+//  because we only react to input events, and not on layout changes, we can have a situation when
+//  Composable is under the cursor, but not hovered. To fix that, we have two ways:
+//  a. Trigger Enter/Exit on any layout change, inside Owner
+//  b. Manually react on layout changes via Modifier.onGloballyPosition, and check something like
+//  LocalPointerPosition.current
+            .pointerInput(interactionSource) {
+                val currentContext = currentCoroutineContext()
+                while (currentContext.isActive) {
+                    val event = awaitPointerEventScope { awaitPointerEvent() }
+                    when (event.type) {
+                        PointerEventType.Enter -> emitEnter()
+                        PointerEventType.Exit -> emitExit()
+                    }
+                }
+            }
+    } else {
+        Modifier
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
index 61f30c1..d85462d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
 import androidx.compose.foundation.interaction.collectIsPressedAsState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -147,12 +148,15 @@
 private object DefaultDebugIndication : Indication {
 
     private class DefaultDebugIndicationInstance(
-        private val isPressed: State<Boolean>
+        private val isPressed: State<Boolean>,
+        private val isHovered: State<Boolean>
     ) : IndicationInstance {
         override fun ContentDrawScope.drawIndication() {
             drawContent()
             if (isPressed.value) {
                 drawRect(color = Color.Black.copy(alpha = 0.3f), size = size)
+            } else if (isHovered.value) {
+                drawRect(color = Color.Black.copy(alpha = 0.1f), size = size)
             }
         }
     }
@@ -160,8 +164,9 @@
     @Composable
     override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
         val isPressed = interactionSource.collectIsPressedAsState()
+        val isHovered = interactionSource.collectIsHoveredAsState()
         return remember(interactionSource) {
-            DefaultDebugIndicationInstance(isPressed)
+            DefaultDebugIndicationInstance(isPressed, isHovered)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/interaction/HoverInteraction.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/interaction/HoverInteraction.kt
new file mode 100644
index 0000000..3ae9cec
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/interaction/HoverInteraction.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 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.interaction
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import kotlinx.coroutines.flow.collect
+
+// An interface, not a sealed class, to allow adding new types here in a safe way (and not break
+// exhaustive when clauses)
+/**
+ * An interaction related to hover events.
+ *
+ * @see androidx.compose.foundation.hoverable
+ * @see Enter
+ * @see Exit
+ */
+interface HoverInteraction : Interaction {
+    /**
+     * An interaction representing a hover event on a component.
+     *
+     * @see androidx.compose.foundation.hoverable
+     * @see Exit
+     */
+    class Enter : HoverInteraction
+
+    /**
+     * An interaction representing a [Enter] event being released on a component.
+     *
+     * @property enter the source [Enter] interaction that is being released
+     *
+     * @see androidx.compose.foundation.hoverable
+     * @see Enter
+     */
+    class Exit(val enter: Enter) : HoverInteraction
+}
+
+/**
+ * Subscribes to this [MutableInteractionSource] and returns a [State] representing whether this
+ * component is hovered or not.
+ *
+ * [HoverInteraction] is typically set by [androidx.compose.foundation.hoverable] and hoverable
+ * components.
+ *
+ * @return [State] representing whether this component is being hovered or not
+ */
+@Composable
+fun InteractionSource.collectIsHoveredAsState(): State<Boolean> {
+    val isHovered = remember { mutableStateOf(false) }
+    LaunchedEffect(this) {
+        val hoverInteractions = mutableListOf<HoverInteraction.Enter>()
+        interactions.collect { interaction ->
+            when (interaction) {
+                is HoverInteraction.Enter -> hoverInteractions.add(interaction)
+                is HoverInteraction.Exit -> hoverInteractions.remove(interaction.enter)
+            }
+            isHovered.value = hoverInteractions.isNotEmpty()
+        }
+    }
+    return isHovered
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
index 044999d..2dbeee8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
@@ -16,7 +16,7 @@
 
 package androidx.compose.foundation.lazy
 
-import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
 
 /**
  * This method finds the sticky header in composedItems list or composes the header item if needed.
@@ -28,12 +28,13 @@
  * @param startContentPadding the padding before the first item in the list
  */
 internal fun findOrComposeLazyListHeader(
-    composedVisibleItems: MutableList<LazyMeasuredItem>,
+    composedVisibleItems: MutableList<LazyListPositionedItem>,
     itemProvider: LazyMeasuredItemProvider,
     headerIndexes: List<Int>,
-    startContentPadding: Int
-): LazyMeasuredItem? {
-    var alreadyVisibleHeaderItem: LazyMeasuredItem? = null
+    startContentPadding: Int,
+    layoutWidth: Int,
+    layoutHeight: Int,
+): LazyListPositionedItem? {
     var currentHeaderOffset: Int = Int.MIN_VALUE
     var nextHeaderOffset: Int = Int.MIN_VALUE
 
@@ -52,9 +53,10 @@
         }
     }
 
-    composedVisibleItems.fastForEach { item ->
+    var indexInComposedVisibleItems = -1
+    composedVisibleItems.fastForEachIndexed { index, item ->
         if (item.index == currentHeaderListPosition) {
-            alreadyVisibleHeaderItem = item
+            indexInComposedVisibleItems = index
             currentHeaderOffset = item.offset
         } else {
             if (item.index == nextHeaderListPosition) {
@@ -68,10 +70,7 @@
         return null
     }
 
-    val headerItem = alreadyVisibleHeaderItem
-        ?: itemProvider.getAndMeasure(DataIndex(currentHeaderListPosition)).also {
-            composedVisibleItems.add(0, it)
-        }
+    val measuredHeaderItem = itemProvider.getAndMeasure(DataIndex(currentHeaderListPosition))
 
     var headerOffset = if (currentHeaderOffset != Int.MIN_VALUE) {
         maxOf(-startContentPadding, currentHeaderOffset)
@@ -81,9 +80,14 @@
     // if we have a next header overlapping with the current header, the next one will be
     // pushing the current one away from the viewport.
     if (nextHeaderOffset != Int.MIN_VALUE) {
-        headerOffset = minOf(headerOffset, nextHeaderOffset - headerItem.size)
+        headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size)
     }
 
-    headerItem.offset = headerOffset
-    return headerItem
+    return measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight).also {
+        if (indexInComposedVisibleItems != -1) {
+            composedVisibleItems[indexInComposedVisibleItems] = it
+        } else {
+            composedVisibleItems.add(0, it)
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 3da26e5..e58d28f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -209,9 +209,10 @@
         val layoutHeight =
             constraints.constrainHeight(if (isVertical) mainAxisUsed else maxCrossAxis)
 
-        calculateItemsOffsets(
+        val positionedItems = calculateItemsOffsets(
             items = visibleItems,
-            mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth,
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
             usedMainAxisSize = mainAxisUsed,
             itemsScrollOffset = visibleItemsScrollOffset,
             isVertical = isVertical,
@@ -224,10 +225,12 @@
 
         val headerItem = if (headerIndexes.isNotEmpty()) {
             findOrComposeLazyListHeader(
-                composedVisibleItems = visibleItems,
+                composedVisibleItems = positionedItems,
                 itemProvider = itemProvider,
                 headerIndexes = headerIndexes,
-                startContentPadding = startContentPadding
+                startContentPadding = startContentPadding,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight
             )
         } else {
             null
@@ -241,17 +244,17 @@
             canScrollForward = mainAxisUsed > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
-                visibleItems.fastForEach {
+                positionedItems.fastForEach {
                     if (it !== headerItem) {
-                        it.place(this, layoutWidth, layoutHeight)
+                        it.place(this)
                     }
                 }
                 // the header item should be placed (drawn) after all other items
-                headerItem?.place(this, layoutWidth, layoutHeight)
+                headerItem?.place(this)
             },
             viewportStartOffset = -startContentPadding,
             viewportEndOffset = maximumVisibleOffset,
-            visibleItemsInfo = visibleItems,
+            visibleItemsInfo = positionedItems,
             totalItemsCount = itemsCount,
         )
     }
@@ -262,7 +265,8 @@
  */
 private fun calculateItemsOffsets(
     items: List<LazyMeasuredItem>,
-    mainAxisLayoutSize: Int,
+    layoutWidth: Int,
+    layoutHeight: Int,
     usedMainAxisSize: Int,
     itemsScrollOffset: Int,
     isVertical: Boolean,
@@ -271,12 +275,15 @@
     reverseLayout: Boolean,
     density: Density,
     layoutDirection: LayoutDirection
-) {
+): MutableList<LazyListPositionedItem> {
+    val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
     val hasSpareSpace = usedMainAxisSize < mainAxisLayoutSize
     if (hasSpareSpace) {
         check(itemsScrollOffset == 0)
     }
 
+    val positionedItems = ArrayList<LazyListPositionedItem>(items.size)
+
     if (hasSpareSpace) {
         val itemsCount = items.size
         val sizes = IntArray(itemsCount) { index ->
@@ -301,13 +308,15 @@
             } else {
                 absoluteOffset
             }
-            item.offset = relativeOffset
+            val addIndex = if (reverseLayout) 0 else positionedItems.size
+            positionedItems.add(addIndex, item.position(relativeOffset, layoutWidth, layoutHeight))
         }
     } else {
         var currentMainAxis = itemsScrollOffset
         items.fastForEach {
-            it.offset = currentMainAxis
+            positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
             currentMainAxis += it.sizeWithSpacings
         }
     }
+    return positionedItems
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
index b6e32ea..1e1f276 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
@@ -19,14 +19,16 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutPlaceable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
 
 /**
  * Represents one measured item of the lazy list. It can in fact consist of multiple placeables
  * if the user emit multiple layout nodes in the item callback.
  */
 internal class LazyMeasuredItem(
-    override val index: Int,
+    val index: Int,
     private val placeables: Array<LazyLayoutPlaceable>,
     private val isVertical: Boolean,
     private val horizontalAlignment: Alignment.Horizontal?,
@@ -40,12 +42,12 @@
      * is usually representing the spacing after the item.
      */
     private val spacing: Int,
-    override val key: Any
-) : LazyListItemInfo {
+    val key: Any
+) {
     /**
      * Sum of the main axis sizes of all the inner placeables.
      */
-    override val size: Int
+    val size: Int
 
     /**
      * Sum of the main axis sizes of all the inner placeables and [spacing].
@@ -57,8 +59,6 @@
      */
     val crossAxisSize: Int
 
-    override var offset: Int = 0
-
     init {
         var mainAxisSize = 0
         var maxCrossAxis = 0
@@ -74,17 +74,18 @@
     }
 
     /**
-     * Perform placing for all the inner placeables at [offset] main axis position. [layoutWidth]
+     * Calculates positions for the inner placeables at [offset] main axis position. [layoutWidth]
      * and [layoutHeight] should be provided to not place placeables which are ended up outside of
      * the viewport (for example one item consist of 2 placeables, and the first one is not going
      * to be visible, so we don't place it as an optimization, but place the second one).
      * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
      */
-    fun place(
-        scope: Placeable.PlacementScope,
+    fun position(
+        offset: Int,
         layoutWidth: Int,
         layoutHeight: Int
-    ) = with(scope) {
+    ): LazyListPositionedItem {
+        val wrappers = mutableListOf<LazyListPlaceableWrapper>()
         val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
         var mainAxisOffset = if (reverseLayout) {
             mainAxisLayoutSize - offset - size
@@ -94,25 +95,72 @@
         var index = if (reverseLayout) placeables.lastIndex else 0
         while (if (reverseLayout) index >= 0 else index < placeables.size) {
             val it = placeables[index].placeable
-            if (reverseLayout) index-- else index++
-            if (isVertical) {
+            val addIndex = if (reverseLayout) 0 else wrappers.size
+            val placeableOffset = if (isVertical) {
                 val x = requireNotNull(horizontalAlignment)
                     .align(it.width, layoutWidth, layoutDirection)
-                if (mainAxisOffset + it.height > -startContentPadding &&
-                    mainAxisOffset < layoutHeight + endContentPadding
-                ) {
-                    it.placeWithLayer(x, mainAxisOffset)
-                }
-                mainAxisOffset += it.height
+                IntOffset(x, mainAxisOffset)
             } else {
                 val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
-                if (mainAxisOffset + it.width > -startContentPadding &&
-                    mainAxisOffset < layoutWidth + endContentPadding
-                ) {
-                    it.placeRelativeWithLayer(mainAxisOffset, y)
+                IntOffset(mainAxisOffset, y)
+            }
+            mainAxisOffset += if (isVertical) it.height else it.width
+            wrappers.add(
+                addIndex,
+                LazyListPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
+            )
+            if (reverseLayout) index-- else index++
+        }
+        return LazyListPositionedItem(
+            offset = offset,
+            index = this.index,
+            key = key,
+            size = size,
+            sizeWithSpacings = sizeWithSpacings,
+            minMainAxisOffset = -startContentPadding,
+            maxMainAxisOffset = mainAxisLayoutSize + endContentPadding,
+            isVertical = isVertical,
+            wrappers = wrappers
+        )
+    }
+}
+
+internal class LazyListPositionedItem(
+    override val offset: Int,
+    override val index: Int,
+    override val key: Any,
+    override val size: Int,
+    val sizeWithSpacings: Int,
+    private val minMainAxisOffset: Int,
+    private val maxMainAxisOffset: Int,
+    private val isVertical: Boolean,
+    private val wrappers: List<LazyListPlaceableWrapper>
+) : LazyListItemInfo {
+
+    fun place(
+        scope: Placeable.PlacementScope,
+    ) = with(scope) {
+        wrappers.fastForEach { wrapper ->
+            val offset = wrapper.offset
+            val placeable = wrapper.placeable
+            if (offset.mainAxis + placeable.mainAxisSize > minMainAxisOffset &&
+                offset.mainAxis < maxMainAxisOffset
+            ) {
+                if (isVertical) {
+                    placeable.placeWithLayer(offset)
+                } else {
+                    placeable.placeRelativeWithLayer(offset)
                 }
-                mainAxisOffset += it.width
             }
         }
     }
+
+    private val IntOffset.mainAxis get() = if (isVertical) y else x
+    private val Placeable.mainAxisSize get() = if (isVertical) height else width
 }
+
+internal class LazyListPlaceableWrapper(
+    val offset: IntOffset,
+    val placeable: Placeable,
+    val parentData: Any?
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index 7fff3e0..8d79b16 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.LocalIndication
 import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.handlePressInteraction
+import androidx.compose.foundation.hoverable
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
@@ -268,5 +269,6 @@
     this
         .then(semantics)
         .indication(interactionSource, indication)
+        .hoverable(interactionSource = interactionSource)
         .then(gestures)
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
index ac939cd..b4caf02 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -46,7 +47,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.input.pointer.consumePositionChange
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChange
@@ -190,7 +190,6 @@
 )
 
 // TODO(demin): do we need to stop dragging if cursor is beyond constraints?
-// TODO(demin): add Interaction.Hovered to interactionSource
 @Composable
 private fun Scrollbar(
     adapter: ScrollbarAdapter,
@@ -211,7 +210,7 @@
     }
 
     var containerSize by remember { mutableStateOf(0) }
-    var isHovered by remember { mutableStateOf(false) }
+    val isHovered by interactionSource.collectIsHoveredAsState()
 
     val isHighlighted by remember {
         derivedStateOf {
@@ -253,21 +252,7 @@
             )
         },
         modifier
-            .pointerInput(Unit) {
-                awaitPointerEventScope {
-                    while (true) {
-                        val event = awaitPointerEvent()
-                        when (event.type) {
-                            PointerEventType.Enter -> {
-                                isHovered = true
-                            }
-                            PointerEventType.Exit -> {
-                                isHovered = false
-                            }
-                        }
-                    }
-                }
-            }
+            .hoverable(interactionSource = interactionSource)
             .scrollOnPressOutsideSlider(isVertical, sliderAdapter, adapter, containerSize),
         measurePolicy
     )
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 24c5c5c9..df51954 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -34,7 +34,7 @@
          * When updating dependencies, make sure to make the an an analogous update in the
          * corresponding block below
          */
-        api("androidx.compose.foundation:foundation:1.0.0")
+        api(project(":compose:foundation:foundation"))
         api(project(":compose:runtime:runtime"))
 
         implementation(libs.kotlinStdlibCommon)
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index 990d1d3..1ae18ca 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.Indication
 import androidx.compose.foundation.IndicationInstance
 import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
@@ -246,8 +247,14 @@
     private var currentInteraction: Interaction? = null
 
     fun handleInteraction(interaction: Interaction, scope: CoroutineScope) {
-        // TODO: handle hover / focus states
+        // TODO: handle focus states
         when (interaction) {
+            is HoverInteraction.Enter -> {
+                interactions.add(interaction)
+            }
+            is HoverInteraction.Exit -> {
+                interactions.remove(interaction.enter)
+            }
             is DragInteraction.Start -> {
                 interactions.add(interaction)
             }
@@ -266,6 +273,7 @@
         if (currentInteraction != newInteraction) {
             if (newInteraction != null) {
                 val targetAlpha = when (interaction) {
+                    is HoverInteraction.Enter -> rippleAlpha.value.hoveredAlpha
                     is DragInteraction.Start -> rippleAlpha.value.draggedAlpha
                     else -> 0f
                 }
@@ -312,26 +320,26 @@
  * @return the [AnimationSpec] used when transitioning to [interaction], either from a previous
  * state, or no state.
  *
- * TODO: handle hover / focus states
+ * TODO: handle focus states
  */
 private fun incomingStateLayerAnimationSpecFor(interaction: Interaction): AnimationSpec<Float> {
-    return if (interaction is DragInteraction.Start) {
-        TweenSpec(durationMillis = 45, easing = LinearEasing)
-    } else {
-        DefaultTweenSpec
+    return when (interaction) {
+        is DragInteraction.Start -> TweenSpec(durationMillis = 45, easing = LinearEasing)
+        is HoverInteraction.Enter -> DefaultTweenSpec
+        else -> DefaultTweenSpec
     }
 }
 
 /**
  * @return the [AnimationSpec] used when transitioning away from [interaction], to no state.
  *
- * TODO: handle hover / focus states
+ * TODO: handle focus states
  */
 private fun outgoingStateLayerAnimationSpecFor(interaction: Interaction?): AnimationSpec<Float> {
-    return if (interaction is DragInteraction.Start) {
-        TweenSpec(durationMillis = 150, easing = LinearEasing)
-    } else {
-        DefaultTweenSpec
+    return when (interaction) {
+        is DragInteraction.Start -> TweenSpec(durationMillis = 150, easing = LinearEasing)
+        is HoverInteraction.Enter -> DefaultTweenSpec
+        else -> DefaultTweenSpec
     }
 }
 
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
index b1b1411..09118d6 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
@@ -105,7 +105,7 @@
  *
  * @property draggedAlpha the alpha used when the ripple is dragged
  * @property focusedAlpha not currently supported
- * @property hoveredAlpha not currently supported
+ * @property hoveredAlpha the alpha used when the ripple is hovered
  * @property pressedAlpha the alpha used when the ripple is pressed
  */
 @Immutable
diff --git a/compose/material/material/api/current.ignore b/compose/material/material/api/current.ignore
new file mode 100644
index 0000000..557e57e
--- /dev/null
+++ b/compose/material/material/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.compose.material.ButtonDefaults#elevation(float, float, float):
+    Removed method androidx.compose.material.ButtonDefaults.elevation(float,float,float)
+RemovedMethod: androidx.compose.material.FloatingActionButtonDefaults#elevation(float, float):
+    Removed method androidx.compose.material.FloatingActionButtonDefaults.elevation(float,float)
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index f22b93c..bc66cd7 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -81,7 +81,7 @@
 
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors(optional long backgroundColor, optional long contentColor, optional long disabledBackgroundColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation, optional float hoveredElevation, optional float focusedElevation);
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
     method public float getIconSize();
     method public float getIconSpacing();
@@ -274,7 +274,7 @@
   }
 
   public final class FloatingActionButtonDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float hoveredElevation, optional float focusedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index e3736e2..1e5c805 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -162,7 +162,7 @@
 
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors(optional long backgroundColor, optional long contentColor, optional long disabledBackgroundColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation, optional float hoveredElevation, optional float focusedElevation);
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
     method public float getIconSize();
     method public float getIconSpacing();
@@ -402,7 +402,7 @@
   }
 
   public final class FloatingActionButtonDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float hoveredElevation, optional float focusedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
diff --git a/compose/material/material/api/restricted_current.ignore b/compose/material/material/api/restricted_current.ignore
new file mode 100644
index 0000000..557e57e
--- /dev/null
+++ b/compose/material/material/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.compose.material.ButtonDefaults#elevation(float, float, float):
+    Removed method androidx.compose.material.ButtonDefaults.elevation(float,float,float)
+RemovedMethod: androidx.compose.material.FloatingActionButtonDefaults#elevation(float, float):
+    Removed method androidx.compose.material.FloatingActionButtonDefaults.elevation(float,float)
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index f22b93c..bc66cd7 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -81,7 +81,7 @@
 
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors(optional long backgroundColor, optional long contentColor, optional long disabledBackgroundColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation, optional float hoveredElevation, optional float focusedElevation);
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
     method public float getIconSize();
     method public float getIconSpacing();
@@ -274,7 +274,7 @@
   }
 
   public final class FloatingActionButtonDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float hoveredElevation, optional float focusedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 4086ced..38af064 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -122,6 +122,7 @@
                 implementation(libs.dexmakerMockito)
                 implementation(libs.mockitoCore)
                 implementation(libs.mockitoKotlin)
+                implementation(libs.testUiautomator)
             }
         }
     }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
index 7194dce..58a3991 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -102,4 +103,22 @@
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "button_ripple")
     }
+
+    @Test
+    fun hover() {
+        rule.setMaterialContent {
+            Box(Modifier.requiredSize(200.dp, 100.dp).wrapContentSize()) {
+                Button(onClick = { }) { }
+            }
+        }
+
+        rule.onNode(hasClickAction())
+            .performMouseInput { enter(center) }
+
+        rule.waitForIdle()
+
+        rule.onRoot()
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_hover")
+    }
 }
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
index 36fdfc7..0d8bbba 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.test.isToggleable
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -205,6 +206,26 @@
         assertToggeableAgainstGolden("checkbox_animateToUnchecked")
     }
 
+    @Test
+    fun checkBoxTest_hover() {
+        rule.setMaterialContent {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = true,
+                    onCheckedChange = { }
+                )
+            }
+        }
+
+        rule.onNode(isToggleable())
+            .performMouseInput { enter(center) }
+
+        rule.waitForIdle()
+
+        assertToggeableAgainstGolden("checkbox_hover")
+    }
+
     private fun assertToggeableAgainstGolden(goldenName: String) {
         // TODO: replace with find(isToggeable()) after b/157687898 is fixed
         rule.onNodeWithTag(wrapperTestTag)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
index 0a6a4d6..08cac4f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.test.hasClickAction
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -121,4 +122,24 @@
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "fab_ripple")
     }
+
+    @Test
+    fun hover() {
+        rule.setMaterialContent {
+            Box(Modifier.requiredSize(100.dp, 100.dp).wrapContentSize()) {
+                FloatingActionButton(onClick = { }) {
+                    Icon(Icons.Filled.Favorite, contentDescription = null)
+                }
+            }
+        }
+
+        rule.onNode(hasClickAction())
+            .performMouseInput { enter(center) }
+
+        rule.waitForIdle()
+
+        rule.onRoot()
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fab_hover")
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index ec572de..d1509f7 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -106,6 +107,28 @@
     }
 
     @Test
+    fun bounded_lightTheme_highLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_bounded_light_highluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
+        )
+    }
+
+    @Test
     fun bounded_lightTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -150,6 +173,28 @@
     }
 
     @Test
+    fun bounded_lightTheme_lowLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_bounded_light_lowluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun bounded_lightTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -194,6 +239,28 @@
     }
 
     @Test
+    fun bounded_darkTheme_highLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_bounded_dark_highluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun bounded_darkTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -239,6 +306,29 @@
     }
 
     @Test
+    fun bounded_darkTheme_lowLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_bounded_dark_lowluminance_hovered",
+            // Low luminance content in dark theme should use a white ripple by default
+            calculateResultingRippleColor(Color.White, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun bounded_darkTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -284,6 +374,28 @@
     }
 
     @Test
+    fun unbounded_lightTheme_highLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_unbounded_light_highluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
+        )
+    }
+
+    @Test
     fun unbounded_lightTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -328,6 +440,28 @@
     }
 
     @Test
+    fun unbounded_lightTheme_lowLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_unbounded_light_lowluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun unbounded_lightTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -372,6 +506,28 @@
     }
 
     @Test
+    fun unbounded_darkTheme_highLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_unbounded_dark_highluminance_hovered",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun unbounded_darkTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -417,6 +573,29 @@
     }
 
     @Test
+    fun unbounded_darkTheme_lowLuminance_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_unbounded_dark_lowluminance_hovered",
+            // Low luminance content in dark theme should use a white ripple by default
+            calculateResultingRippleColor(Color.White, rippleOpacity = 0.04f)
+        )
+    }
+
+    @Test
     fun unbounded_darkTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -491,6 +670,57 @@
     }
 
     @Test
+    fun customRippleTheme_hovered() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val rippleColor = Color.Red
+        val expectedAlpha = 0.5f
+        val rippleAlpha = RippleAlpha(expectedAlpha, expectedAlpha, expectedAlpha, expectedAlpha)
+
+        val rippleTheme = object : RippleTheme {
+            @Composable
+            override fun defaultColor() = rippleColor
+
+            @Composable
+            override fun rippleAlpha() = rippleAlpha
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            MaterialTheme {
+                CompositionLocalProvider(LocalRippleTheme provides rippleTheme) {
+                    Surface(contentColor = contentColor) {
+                        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                            RippleBoxWithBackground(
+                                interactionSource,
+                                rememberRipple(),
+                                bounded = true
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            rippleColor,
+            rippleOpacity = expectedAlpha
+        )
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            HoverInteraction.Enter(),
+            "ripple_customtheme_hovered",
+            expectedColor
+        )
+    }
+
+    @Test
     fun customRippleTheme_dragged() {
         val interactionSource = MutableInteractionSource()
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
index 3b3c67c..99fe9e3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.test.isSelectable
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -99,6 +100,20 @@
     }
 
     @Test
+    fun radioButtonTest_hovered() {
+        rule.setMaterialContent {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = false, onClick = {})
+            }
+        }
+        rule.onNodeWithTag(wrapperTestTag).performMouseInput {
+            enter(center)
+        }
+
+        assertSelectableAgainstGolden("radioButton_hovered")
+    }
+
+    @Test
     fun radioButtonTest_disabled_selected() {
         rule.setMaterialContent {
             Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 43f12ce..ad24ccf 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.test.isToggleable
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -235,6 +236,25 @@
         assertToggeableAgainstGolden("switch_animateToUnchecked")
     }
 
+    @Test
+    fun switchTest_hover() {
+        rule.setMaterialContent {
+            Box(wrapperModifier) {
+                Switch(
+                    checked = true,
+                    onCheckedChange = { }
+                )
+            }
+        }
+
+        rule.onNode(isToggleable())
+            .performMouseInput { enter(center) }
+
+        rule.waitForIdle()
+
+        assertToggeableAgainstGolden("switch_hover")
+    }
+
     private fun assertToggeableAgainstGolden(goldenName: String) {
         rule.onNodeWithTag(wrapperTestTag)
             .captureToImage()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index cf6ce1b73..47e85a9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/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.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -327,7 +328,7 @@
      */
     val IconSpacing = 8.dp
 
-    // TODO: b/152525426 add support for focused and hovered states
+    // TODO: b/152525426 add support for focused states
     /**
      * Creates a [ButtonElevation] that will animate between the provided values according to the
      * Material specification for a [Button].
@@ -338,19 +339,47 @@
      * is pressed.
      * @param disabledElevation the elevation to use when the [Button] is not enabled.
      */
+    @Deprecated("Use another overload of elevation", level = DeprecationLevel.HIDDEN)
     @Composable
     fun elevation(
         defaultElevation: Dp = 2.dp,
         pressedElevation: Dp = 8.dp,
-        // focused: Dp = 4.dp,
-        // hovered: Dp = 4.dp,
         disabledElevation: Dp = 0.dp
+    ): ButtonElevation = elevation(
+        defaultElevation,
+        pressedElevation,
+        disabledElevation,
+        hoveredElevation = 4.dp,
+        focusedElevation = 4.dp,
+    )
+
+    /**
+     * Creates a [ButtonElevation] that will animate between the provided values according to the
+     * Material specification for a [Button].
+     *
+     * @param defaultElevation the elevation to use when the [Button] is enabled, and has no
+     * other [Interaction]s.
+     * @param pressedElevation the elevation to use when the [Button] is enabled and
+     * is pressed.
+     * @param disabledElevation the elevation to use when the [Button] is not enabled.
+     * @param hoveredElevation the elevation to use when the [Button] is enabled and is hovered.
+     * @param focusedElevation not currently supported.
+     */
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun elevation(
+        defaultElevation: Dp = 2.dp,
+        pressedElevation: Dp = 8.dp,
+        disabledElevation: Dp = 0.dp,
+        hoveredElevation: Dp = 4.dp,
+        focusedElevation: Dp = 4.dp,
     ): ButtonElevation {
-        return remember(defaultElevation, pressedElevation, disabledElevation) {
+        return remember(defaultElevation, pressedElevation, disabledElevation, hoveredElevation) {
             DefaultButtonElevation(
                 defaultElevation = defaultElevation,
                 pressedElevation = pressedElevation,
-                disabledElevation = disabledElevation
+                disabledElevation = disabledElevation,
+                hoveredElevation = hoveredElevation,
             )
         }
     }
@@ -461,6 +490,7 @@
     private val defaultElevation: Dp,
     private val pressedElevation: Dp,
     private val disabledElevation: Dp,
+    private val hoveredElevation: Dp,
 ) : ButtonElevation {
     @Composable
     override fun elevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
@@ -468,6 +498,12 @@
         LaunchedEffect(interactionSource) {
             interactionSource.interactions.collect { interaction ->
                 when (interaction) {
+                    is HoverInteraction.Enter -> {
+                        interactions.add(interaction)
+                    }
+                    is HoverInteraction.Exit -> {
+                        interactions.remove(interaction.enter)
+                    }
                     is PressInteraction.Press -> {
                         interactions.add(interaction)
                     }
@@ -488,6 +524,7 @@
         } else {
             when (interaction) {
                 is PressInteraction.Press -> pressedElevation
+                is HoverInteraction.Enter -> hoveredElevation
                 else -> defaultElevation
             }
         }
@@ -503,6 +540,7 @@
             LaunchedEffect(target) {
                 val lastInteraction = when (animatable.targetValue) {
                     pressedElevation -> PressInteraction.Press(Offset.Zero)
+                    hoveredElevation -> HoverInteraction.Enter()
                     else -> null
                 }
                 animatable.animateElevation(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index 1a8d1e1..56e3198 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.core.FastOutSlowInEasing
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.ui.unit.Dp
@@ -79,6 +80,7 @@
         return when (interaction) {
             is PressInteraction.Press -> DefaultIncomingSpec
             is DragInteraction.Start -> DefaultIncomingSpec
+            is HoverInteraction.Enter -> DefaultIncomingSpec
             else -> null
         }
     }
@@ -93,7 +95,7 @@
         return when (interaction) {
             is PressInteraction.Press -> DefaultOutgoingSpec
             is DragInteraction.Start -> DefaultOutgoingSpec
-            // TODO: use [HoveredOutgoingSpec] when hovered
+            is HoverInteraction.Enter -> HoveredOutgoingSpec
             else -> null
         }
     }
@@ -109,7 +111,6 @@
     easing = CubicBezierEasing(0.40f, 0.00f, 0.60f, 1.00f)
 )
 
-@Suppress("unused")
 private val HoveredOutgoingSpec = TweenSpec<Dp>(
     durationMillis = 120,
     easing = CubicBezierEasing(0.40f, 0.00f, 0.60f, 1.00f)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index e6bf730..ef23afe 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
+import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -205,7 +206,7 @@
  * Contains the default values used by [FloatingActionButton]
  */
 object FloatingActionButtonDefaults {
-    // TODO: b/152525426 add support for focused and hovered states
+    // TODO: b/152525426 add support for focused states
     /**
      * Creates a [FloatingActionButtonElevation] that will animate between the provided values
      * according to the Material specification.
@@ -215,17 +216,43 @@
      * @param pressedElevation the elevation to use when the [FloatingActionButton] is
      * pressed.
      */
+    @Deprecated("Use another overload of elevation", level = DeprecationLevel.HIDDEN)
     @Composable
     fun elevation(
         defaultElevation: Dp = 6.dp,
-        pressedElevation: Dp = 12.dp
-        // focused: Dp = 8.dp,
-        // hovered: Dp = 8.dp,
+        pressedElevation: Dp = 12.dp,
+    ): FloatingActionButtonElevation = elevation(
+        defaultElevation,
+        pressedElevation,
+        hoveredElevation = 8.dp,
+        focusedElevation = 8.dp,
+    )
+
+    /**
+     * Creates a [FloatingActionButtonElevation] that will animate between the provided values
+     * according to the Material specification.
+     *
+     * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
+     * [Interaction]s
+     * @param pressedElevation the elevation to use when the [FloatingActionButton] is
+     * pressed.
+     * @param hoveredElevation the elevation to use when the [FloatingActionButton] is
+     * hovered.
+     * @param focusedElevation not currently supported.
+     */
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun elevation(
+        defaultElevation: Dp = 6.dp,
+        pressedElevation: Dp = 12.dp,
+        hoveredElevation: Dp = 8.dp,
+        focusedElevation: Dp = 8.dp,
     ): FloatingActionButtonElevation {
-        return remember(defaultElevation, pressedElevation) {
+        return remember(defaultElevation, pressedElevation, hoveredElevation) {
             DefaultFloatingActionButtonElevation(
                 defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation
+                pressedElevation = pressedElevation,
+                hoveredElevation = hoveredElevation,
             )
         }
     }
@@ -238,6 +265,7 @@
 private class DefaultFloatingActionButtonElevation(
     private val defaultElevation: Dp,
     private val pressedElevation: Dp,
+    private val hoveredElevation: Dp
 ) : FloatingActionButtonElevation {
     @Composable
     override fun elevation(interactionSource: InteractionSource): State<Dp> {
@@ -245,6 +273,12 @@
         LaunchedEffect(interactionSource) {
             interactionSource.interactions.collect { interaction ->
                 when (interaction) {
+                    is HoverInteraction.Enter -> {
+                        interactions.add(interaction)
+                    }
+                    is HoverInteraction.Exit -> {
+                        interactions.remove(interaction.enter)
+                    }
                     is PressInteraction.Press -> {
                         interactions.add(interaction)
                     }
@@ -262,6 +296,7 @@
 
         val target = when (interaction) {
             is PressInteraction.Press -> pressedElevation
+            is HoverInteraction.Enter -> hoveredElevation
             else -> defaultElevation
         }
 
@@ -270,6 +305,7 @@
         LaunchedEffect(target) {
             val lastInteraction = when (animatable.targetValue) {
                 pressedElevation -> PressInteraction.Press(Offset.Zero)
+                hoveredElevation -> HoverInteraction.Enter()
                 else -> null
             }
             animatable.animateElevation(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index fdee814..bfec8ff 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -31,6 +31,7 @@
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.forEachGesture
 import androidx.compose.foundation.gestures.horizontalDrag
+import androidx.compose.foundation.hoverable
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.Interaction
@@ -625,6 +626,7 @@
                     interactionSource = interactionSource,
                     indication = rememberRipple(bounded = false, radius = ThumbRippleRadius)
                 )
+                .hoverable(interactionSource = interactionSource)
                 .shadow(if (enabled) elevation else 0.dp, CircleShape, clip = false)
                 .background(colors.thumbColor(enabled).value, CircleShape)
         )
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 05ecf1a..ad1b9a4 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -106,8 +106,8 @@
   }
 
   public final class SurfaceKt {
-    method @androidx.compose.runtime.Composable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable 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 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 @androidx.compose.runtime.Composable public static void Surface(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, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable 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();
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 28ecd4a..b246af7 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -121,8 +121,8 @@
   }
 
   public final class SurfaceKt {
-    method @androidx.compose.runtime.Composable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable 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 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 @androidx.compose.runtime.Composable public static void Surface(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, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable 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();
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 05ecf1a..ad1b9a4 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -106,8 +106,8 @@
   }
 
   public final class SurfaceKt {
-    method @androidx.compose.runtime.Composable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable 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 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 @androidx.compose.runtime.Composable public static void Surface(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, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable 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();
   }
 
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 3011778..007999b2 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
@@ -22,17 +22,21 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.asAndroidBitmap
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.testTag
@@ -192,6 +196,59 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun absoluteElevationIsNotUsedForShadows() {
+        rule.setMaterialContent {
+            Column {
+                Box(
+                    Modifier
+                        .padding(10.dp)
+                        .size(10.dp, 10.dp)
+                        .semantics(mergeDescendants = true) {}
+                        .testTag("top level")
+                ) {
+                    Surface(
+                        Modifier.fillMaxSize().padding(0.dp),
+                        tonalElevation = 2.dp,
+                        shadowElevation = 2.dp,
+                        color = Color.Blue,
+                        content = {}
+                    )
+                }
+
+                // Set LocalAbsoluteTonalElevation to increase the absolute elevation
+                CompositionLocalProvider(
+                    LocalAbsoluteTonalElevation provides 2.dp
+                ) {
+                    Box(
+                        Modifier
+                            .padding(10.dp)
+                            .size(10.dp, 10.dp)
+                            .semantics(mergeDescendants = true) {}
+                            .testTag("nested")
+                    ) {
+                        Surface(
+                            Modifier.fillMaxSize().padding(0.dp),
+                            tonalElevation = 0.dp,
+                            shadowElevation = 2.dp,
+                            color = Color.Blue,
+                            content = {}
+                        )
+                    }
+                }
+            }
+        }
+
+        val topLevelSurfaceBitmap = rule.onNodeWithTag("top level").captureToImage()
+        val nestedSurfaceBitmap = rule.onNodeWithTag("nested").captureToImage()
+            .asAndroidBitmap()
+
+        topLevelSurfaceBitmap.assertPixels {
+            Color(nestedSurfaceBitmap.getPixel(it.x, it.y))
+        }
+    }
+
     /**
      * Tests that composed modifiers applied to Surface are applied within the changes to
      * [LocalContentColor], so they can consume the updated values.
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 eb9fe03..d2b90c1 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
@@ -32,6 +32,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
@@ -89,6 +90,10 @@
  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation (surface
  * blended with more primary) will result in a darker surface color in light theme and lighter color
  * in dark theme.
+ * @param shadowElevation The size of the shadow below the surface. To prevent shadow creep, only
+ * apply shadow elevation when absolutely necessary, such as when the surface requires visual
+ * separation from a patterned background. Note that It will not affect z index of the Surface.
+ * If you want to change the drawing order you can use `Modifier.zIndex`.
  * @param border Optional border to draw on top of the surface
  */
 @Composable
@@ -98,6 +103,7 @@
     color: Color = MaterialTheme.colorScheme.surface,
     contentColor: Color = contentColorFor(color),
     tonalElevation: Dp = 0.dp,
+    shadowElevation: Dp = 0.dp,
     border: BorderStroke? = null,
     content: @Composable () -> Unit
 ) {
@@ -107,6 +113,7 @@
         color = color,
         contentColor = contentColor,
         tonalElevation = tonalElevation,
+        shadowElevation = shadowElevation,
         border = border,
         content = content,
         clickAndSemanticsModifier =
@@ -168,6 +175,8 @@
  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation (surface
  * blended with more primary) will result in a darker surface color in light theme and lighter color
  * in dark theme.
+ * @param shadowElevation The size of the shadow below the surface. Note that It will not affect z index
+ * of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
  * you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
@@ -190,6 +199,7 @@
     color: Color = MaterialTheme.colorScheme.surface,
     contentColor: Color = contentColorFor(color),
     tonalElevation: Dp = 0.dp,
+    shadowElevation: Dp = 0.dp,
     border: BorderStroke? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     indication: Indication? = LocalIndication.current,
@@ -204,6 +214,7 @@
         color = color,
         contentColor = contentColor,
         tonalElevation = tonalElevation,
+        shadowElevation = shadowElevation,
         border = border,
         content = content,
         clickAndSemanticsModifier =
@@ -220,12 +231,13 @@
 
 @Composable
 private fun Surface(
-    modifier: Modifier = Modifier,
-    shape: Shape = RectangleShape,
-    border: BorderStroke? = null,
-    color: Color = MaterialTheme.colorScheme.primary,
-    contentColor: Color = contentColorFor(color),
-    tonalElevation: Dp = 0.dp, // This will be used to compute surface tonal colors
+    modifier: Modifier,
+    shape: Shape,
+    color: Color,
+    contentColor: Color,
+    border: BorderStroke?,
+    tonalElevation: Dp, // This will be used to compute surface tonal colors
+    shadowElevation: Dp,
     clickAndSemanticsModifier: Modifier,
     content: @Composable () -> Unit
 ) {
@@ -242,6 +254,7 @@
     ) {
         Box(
             modifier
+                .shadow(shadowElevation, shape, clip = false)
                 .then(if (border != null) Modifier.border(border, shape) else Modifier)
                 .background(color = backgroundColor, shape = shape)
                 .clip(shape)
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ec30e66..a5f0b94 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -287,6 +287,9 @@
     method public boolean moveFocus(int focusDirection);
   }
 
+  public final class FocusManagerKt {
+  }
+
   public final class FocusModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -333,6 +336,16 @@
     method public static androidx.compose.ui.Modifier focusOrder(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusOrder,kotlin.Unit> focusOrderReceiver);
   }
 
+  public interface FocusProperties {
+    method public boolean getCanFocus();
+    method public void setCanFocus(boolean canFocus);
+    property public abstract boolean canFocus;
+  }
+
+  public final class FocusPropertiesKt {
+    method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
+  }
+
   public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 6f69075..a0035e9 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -358,6 +358,9 @@
     method public boolean moveFocus(int focusDirection);
   }
 
+  public final class FocusManagerKt {
+  }
+
   public final class FocusModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -404,6 +407,16 @@
     method public static androidx.compose.ui.Modifier focusOrder(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusOrder,kotlin.Unit> focusOrderReceiver);
   }
 
+  public interface FocusProperties {
+    method public boolean getCanFocus();
+    method public void setCanFocus(boolean canFocus);
+    property public abstract boolean canFocus;
+  }
+
+  public final class FocusPropertiesKt {
+    method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
+  }
+
   public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 28214e6..e34df74 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -287,6 +287,9 @@
     method public boolean moveFocus(int focusDirection);
   }
 
+  public final class FocusManagerKt {
+  }
+
   public final class FocusModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -333,6 +336,16 @@
     method public static androidx.compose.ui.Modifier focusOrder(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusOrder,kotlin.Unit> focusOrderReceiver);
   }
 
+  public interface FocusProperties {
+    method public boolean getCanFocus();
+    method public void setCanFocus(boolean canFocus);
+    property public abstract boolean canFocus;
+  }
+
+  public final class FocusPropertiesKt {
+    method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
+  }
+
   public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index b19fa00..f415db7 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -53,6 +53,7 @@
 import androidx.compose.ui.demos.gestures.ScrollGestureFilterDemo
 import androidx.compose.ui.demos.gestures.VerticalScrollerInDrawerDemo
 import androidx.compose.ui.demos.input.TouchModeDemo
+import androidx.compose.ui.demos.focus.ConditionalFocusabilityDemo
 import androidx.compose.ui.demos.scroll.BringIntoViewDemo
 import androidx.compose.ui.demos.keyinput.KeyInputDemo
 import androidx.compose.ui.demos.keyinput.InterceptEnterToSendMessageDemo
@@ -132,7 +133,8 @@
         ComposableDemo("Custom Focus Order") { CustomFocusOrderDemo() },
         ComposableDemo("FocusManager.moveFocus()") { FocusManagerMoveFocusDemo() },
         ComposableDemo("Capture/Free Focus") { CaptureFocusDemo() },
-        ComposableDemo("Focus In Scrollable Row") { ScrollableRowFocusDemo() }
+        ComposableDemo("Focus In Scrollable Row") { ScrollableRowFocusDemo() },
+        ComposableDemo("Conditional Focusability") { ConditionalFocusabilityDemo() }
     )
 )
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt
new file mode 100644
index 0000000..08380dd
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2021 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.demos.focus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
+import androidx.compose.material.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.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color.Companion.Gray
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.input.InputMode.Companion.Keyboard
+import androidx.compose.ui.platform.LocalInputModeManager
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun ConditionalFocusabilityDemo() {
+    val localInputModeManager = LocalInputModeManager.current
+    val (item1, item2, item3, item4) = remember { FocusRequester.createRefs() }
+    Column {
+        Text(
+            """
+             The items here are focusable. Use the
+             keyboard or DPad to move focus among them.
+
+             The 1st item is focusable in all modes.
+             Notice that when you touch the screen it
+             does not lose focus like the other items.
+
+             The 2nd item's focusability can be
+             controlled by using the button next to it.
+
+             The 3rd item is not focusable in touch mode.
+
+             The 4th item is not focusable in touch mode,
+             but clicking on it will request the system
+             to switch to keyboard mode, and then call
+             request focus.
+             """.trimIndent()
+        )
+        Text(
+            text = "Focusable in all modes",
+            modifier = Modifier
+                .focusAwareBackground()
+                .focusRequester(item1)
+                .clickable { item1.requestFocus() }
+                .focusable()
+        )
+        Row {
+            var item2active by remember { mutableStateOf(false) }
+            Text(
+                text = "focusable item that is " +
+                    "${if (item2active) "activated" else "deactivated"}",
+                modifier = Modifier
+                    .focusAwareBackground()
+                    .focusRequester(item2)
+                    .clickable { item2.requestFocus() }
+                    .focusProperties { canFocus = item2active }
+                    .focusable()
+            )
+            Button(onClick = { item2active = !item2active }) {
+                Text("${if (item2active) "deactivate" else "activate"} item 2")
+            }
+        }
+        Text(
+            text = "Focusable in keyboard mode",
+            modifier = Modifier
+                .focusAwareBackground()
+                .focusRequester(item3)
+                .clickable { item3.requestFocus() }
+                .focusProperties { canFocus = localInputModeManager.inputMode == Keyboard }
+                .focusable()
+        )
+        Text(
+            text = "Request focus by touch",
+            modifier = Modifier
+                .focusAwareBackground()
+                .focusRequester(item4)
+                .clickable {
+                    if (localInputModeManager.requestInputMode(Keyboard)) {
+                        item4.requestFocus()
+                    }
+                }
+                .focusProperties { canFocus = localInputModeManager.inputMode == Keyboard }
+                .focusable()
+        )
+    }
+}
+
+private fun Modifier.focusAwareBackground() = composed {
+    var color by remember { mutableStateOf(Gray) }
+    Modifier
+        .padding(10.dp)
+        .size(150.dp, 50.dp)
+        .background(color)
+        .onFocusChanged { color = if (it.isFocused) Red else Gray }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
index 8f0da4a..9d4ab33 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusOrder
+import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.focusTarget
 import androidx.compose.ui.focus.onFocusChanged
@@ -47,7 +48,9 @@
 import androidx.compose.ui.graphics.Color.Companion.Green
 import androidx.compose.ui.graphics.Color.Companion.Red
 import androidx.compose.ui.graphics.Color.Companion.Transparent
+import androidx.compose.ui.input.InputMode.Companion.Touch
 import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -205,3 +208,21 @@
         }
     }
 }
+
+@Sampled
+@Composable
+fun FocusPropertiesSample() {
+    Column {
+        // Always focusable.
+        Box(modifier = Modifier
+            .focusProperties { canFocus = true }
+            .focusTarget()
+        )
+        // Only focusable in non-touch mode.
+        val inputModeManager = LocalInputModeManager.current
+        Box(modifier = Modifier
+            .focusProperties { canFocus = inputModeManager.inputMode != Touch }
+            .focusTarget()
+        )
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index 360b423..d139569 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Bitmap
 import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -101,7 +102,7 @@
 class PainterModifierTest {
 
     val containerWidth = 100.0f
-    val containerHeight = 100.0f
+    private val containerHeight = 100.0f
 
     @get:Rule
     val rule = createComposeRule()
@@ -751,6 +752,7 @@
     }
 }
 
+@RequiresApi(Build.VERSION_CODES.O)
 private fun ComposeTestRule.obtainScreenshotBitmap(width: Int, height: Int = width): Bitmap {
     val bitmap = onRoot().captureToImage()
     assertEquals(width, bitmap.width)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
index c42fa1c..0dcf0c8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
@@ -16,12 +16,12 @@
 
 package androidx.compose.ui.focus
 
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -60,6 +60,7 @@
         rule.runOnIdle {
             assertThat(success).isTrue()
             assertThat(focusState.isCaptured).isTrue()
+            assertThat(focusState.isFocused).isTrue()
         }
     }
 
@@ -85,6 +86,7 @@
         // Assert.
         rule.runOnIdle {
             assertThat(success).isFalse()
+            assertThat(focusState.isCaptured).isFalse()
             assertThat(focusState.hasFocus).isTrue()
         }
     }
@@ -116,7 +118,7 @@
     }
 
     @Test
-    fun disabled_captureFocus_retainsStateAsDisabled() {
+    fun deactivated_captureFocus_retainsStateAsDeactivated() {
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
@@ -125,7 +127,8 @@
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(FocusModifier(Disabled))
+                    .focusProperties { canFocus = false }
+                    .focusable()
             )
         }
 
@@ -137,7 +140,45 @@
         // Assert.
         rule.runOnIdle {
             assertThat(success).isFalse()
-            assertThat(focusState).isEqualTo(Disabled)
+            assertThat(focusState.isCaptured).isFalse()
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun deactivatedParent_captureFocus_retainsStateAsDeactivatedParent() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val initialFocus = FocusRequester()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .focusRequester(focusRequester)
+                    .focusProperties { canFocus = false }
+                    .focusable()
+            ) {
+                Box(modifier = Modifier
+                    .focusRequester(initialFocus)
+                    .focusable()
+                )
+            }
+        }
+        rule.runOnIdle { initialFocus.requestFocus() }
+
+        // Act.
+        val success = rule.runOnIdle {
+            focusRequester.captureFocus()
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(success).isFalse()
+            assertThat(focusState.isCaptured).isFalse()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
         }
     }
 
@@ -163,7 +204,11 @@
         // Assert.
         rule.runOnIdle {
             assertThat(success).isFalse()
+            assertThat(focusState.isCaptured).isFalse()
             assertThat(focusState.isFocused).isFalse()
         }
     }
 }
+
+private val FocusState.isDeactivated: Boolean
+    get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
index 7f981ea..c556531 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
@@ -18,10 +18,12 @@
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.filters.SmallTest
@@ -257,11 +259,11 @@
     }
 
     @Test
-    fun Disabled_isUnchanged() {
+    fun Deactivated_isUnchanged() {
         // Arrange.
-        val modifier = FocusModifier(Disabled)
+        val modifier = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier = modifier)
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(modifier))
         }
 
         // Act.
@@ -272,7 +274,131 @@
         // Assert.
         rule.runOnIdle {
             assertThat(cleared).isTrue()
-            assertThat(modifier.focusState).isEqualTo(Disabled)
+            assertThat(modifier.focusState.isDeactivated).isTrue()
         }
     }
-}
\ No newline at end of file
+
+    @Test(expected = IllegalArgumentException::class)
+    fun deactivatedParent_noFocusedChild_throwsException() {
+        // Arrange.
+        val modifier = FocusModifier(DeactivatedParent)
+        rule.setFocusableContent {
+            Box(modifier = modifier)
+        }
+
+        // Act.
+        rule.runOnIdle {
+            modifier.focusNode.clearFocus(forced)
+        }
+    }
+
+    @Test
+    fun deactivatedParent_isClearedAndRemovedFromParentsFocusedChild() {
+        // Arrange.
+        val parent = FocusModifier(ActiveParent)
+        val modifier = FocusModifier(ActiveParent)
+        val child = FocusModifier(Active)
+        rule.setFocusableContent {
+            Box(modifier = parent) {
+                Box(modifier = Modifier.focusProperties { canFocus = false }.then(modifier)) {
+                    Box(modifier = child)
+                }
+            }
+            SideEffect {
+                parent.focusedChild = modifier.focusNode
+                modifier.focusedChild = child.focusNode
+            }
+        }
+
+        // Act.
+        val cleared = rule.runOnIdle {
+            modifier.focusNode.clearFocus(forced)
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(cleared).isTrue()
+            assertThat(modifier.focusedChild).isNull()
+            assertThat(modifier.focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun deactivatedParent_withDeactivatedGrandParent_isClearedAndRemovedFromParentsFocusedChild() {
+        // Arrange.
+        val parent = FocusModifier(ActiveParent)
+        val modifier = FocusModifier(ActiveParent)
+        val child = FocusModifier(Active)
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(parent)) {
+                Box(modifier = Modifier.focusProperties { canFocus = false }.then(modifier)) {
+                    Box(modifier = child)
+                }
+            }
+            SideEffect {
+                parent.focusedChild = modifier.focusNode
+                modifier.focusedChild = child.focusNode
+            }
+        }
+
+        // Act.
+        val cleared = rule.runOnIdle {
+            modifier.focusNode.clearFocus(forced)
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(cleared).isTrue()
+            assertThat(modifier.focusedChild).isNull()
+            assertThat(modifier.focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun deactivatedParent_clearsEntireHierarchy() {
+        // Arrange.
+        val modifier = FocusModifier(ActiveParent)
+        val child = FocusModifier(ActiveParent)
+        val grandchild = FocusModifier(ActiveParent)
+        val greatGrandchild = FocusModifier(ActiveParent)
+        val greatGreatGrandchild = FocusModifier(Active)
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(modifier)) {
+                Box(modifier = child) {
+                    Box(modifier = Modifier
+                        .focusProperties { canFocus = false }
+                        .then(grandchild)
+                    ) {
+                        Box(modifier = greatGrandchild) {
+                            Box(modifier = greatGreatGrandchild)
+                        }
+                    }
+                }
+            }
+            SideEffect {
+                modifier.focusedChild = child.focusNode
+                child.focusedChild = grandchild.focusNode
+                grandchild.focusedChild = greatGrandchild.focusNode
+                greatGrandchild.focusedChild = greatGreatGrandchild.focusNode
+            }
+        }
+
+        // Act.
+        val cleared = rule.runOnIdle {
+            modifier.focusNode.clearFocus(forced)
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(cleared).isTrue()
+            assertThat(modifier.focusedChild).isNull()
+            assertThat(child.focusedChild).isNull()
+            assertThat(grandchild.focusedChild).isNull()
+            assertThat(modifier.focusState).isEqualTo(Deactivated)
+            assertThat(child.focusState).isEqualTo(Inactive)
+            assertThat(grandchild.focusState).isEqualTo(Deactivated)
+            assertThat(greatGrandchild.focusState).isEqualTo(Inactive)
+            assertThat(greatGreatGrandchild.focusState).isEqualTo(Inactive)
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt
new file mode 100644
index 0000000..797119d
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2021 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 androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+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 DeactivatedFocusPropertiesTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun notDeactivatedByDefault() {
+        // Arrange.
+        var isDeactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .onFocusChanged { isDeactivated = it.isDeactivated }
+                .focusTarget()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(isDeactivated).isFalse() }
+    }
+
+    @Test
+    fun initializedAsNotDeactivated() {
+        // Arrange.
+        var deactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { canFocus = true }
+                .onFocusChanged { deactivated = it.isDeactivated }
+                .focusTarget()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(deactivated).isFalse() }
+    }
+
+    @Test
+    fun initializedAsDeactivated() {
+        // Arrange.
+        var isDeactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { isDeactivated = true }
+                .onFocusChanged { isDeactivated = it.isDeactivated }
+                .focusTarget()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(isDeactivated).isTrue() }
+    }
+
+    @Test
+    fun leftMostDeactivatedPropertyTakesPrecedence() {
+        // Arrange.
+        var deactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { canFocus = false }
+                .focusProperties { canFocus = true }
+                .onFocusChanged { deactivated = it.isDeactivated }
+                .focusTarget()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(deactivated).isTrue() }
+    }
+
+    @Test
+    fun leftMostNonDeactivatedPropertyTakesPrecedence() {
+        // Arrange.
+        var deactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { canFocus = true }
+                .focusProperties { canFocus = false }
+                .onFocusChanged { deactivated = it.isDeactivated }
+                .focusTarget()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(deactivated).isFalse() }
+    }
+
+    @Test
+    fun ParentsDeactivatedPropertyTakesPrecedence() {
+        // Arrange.
+        var deactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = false }) {
+                Box(modifier = Modifier
+                    .focusProperties { canFocus = true }
+                    .onFocusChanged { deactivated = it.isDeactivated }
+                    .focusTarget()
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(deactivated).isTrue() }
+    }
+
+    @Test
+    fun ParentsNotDeactivatedPropertyTakesPrecedence() {
+        // Arrange.
+        var deactivated: Boolean? = null
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = true }) {
+                Box(modifier = Modifier
+                    .focusProperties { canFocus = false }
+                    .onFocusChanged { deactivated = it.isDeactivated }
+                    .focusTarget()
+                )
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(deactivated).isFalse() }
+    }
+
+    @Test
+    fun deactivatedItemDoesNotGainFocus() {
+        // Arrange.
+        var isFocused: Boolean? = null
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { canFocus = false }
+                .focusRequester(focusRequester)
+                .onFocusChanged { isFocused = it.isFocused }
+                .focusTarget()
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(isFocused).isFalse() }
+    }
+
+    @Test
+    fun deactivatedFocusPropertiesOnNonFocusableParentAppliesToAllChildren() {
+        // Arrange.
+        var isParentDeactivated: Boolean? = null
+        var isChild1Deactivated: Boolean? = null
+        var isChild2Deactivated: Boolean? = null
+        var isGrandChildDeactivated: Boolean? = null
+        rule.setFocusableContent {
+            Column(modifier = Modifier
+                .focusProperties { canFocus = false }
+                .onFocusChanged { isParentDeactivated = it.isDeactivated }
+            ) {
+                Box(modifier = Modifier
+                    .onFocusChanged { isChild1Deactivated = it.isDeactivated }
+                    .focusTarget()
+                )
+                Box(modifier = Modifier
+                        .onFocusChanged { isChild2Deactivated = it.isDeactivated }
+                        .focusTarget()
+                ) {
+                    Box(modifier = Modifier
+                        .onFocusChanged { isGrandChildDeactivated = it.isDeactivated }
+                        .focusTarget()
+                    )
+                }
+            }
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(isParentDeactivated).isTrue() }
+        rule.runOnIdle { assertThat(isChild1Deactivated).isTrue() }
+        rule.runOnIdle { assertThat(isChild2Deactivated).isTrue() }
+        rule.runOnIdle { assertThat(isGrandChildDeactivated).isFalse() }
+    }
+
+    @Test
+    fun focusedItemLosesFocusWhenDeactivated() {
+        // Arrange.
+        var isFocused: Boolean? = null
+        val focusRequester = FocusRequester()
+        var deactivated by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(modifier = Modifier
+                .focusProperties { canFocus = !deactivated }
+                .focusRequester(focusRequester)
+                .onFocusChanged { isFocused = it.isFocused }
+                .focusTarget()
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(isFocused).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { deactivated = true }
+
+        // Assert.
+        rule.runOnIdle { assertThat(isFocused).isFalse() }
+    }
+}
+
+private val FocusState.isDeactivated: Boolean
+    get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
index 93fd9b8..f3038a1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
@@ -18,41 +18,60 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.graphics.Color.Companion.Red
 import androidx.compose.ui.test.junit4.createComposeRule
-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
+import org.junit.runners.Parameterized
 
 @MediumTest
-@RunWith(AndroidJUnit4::class)
-class FindFocusableChildrenTest {
+@RunWith(Parameterized::class)
+class FindFocusableChildrenTest(private val excludeDeactivated: Boolean) {
     @get:Rule
     val rule = createComposeRule()
 
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "excludeDeactivated = {0}")
+        fun initParameters() = listOf(true, false)
+    }
+
     @Test
     fun returnsFirstFocusNodeInModifierChain() {
         val focusModifier1 = FocusModifier(Inactive)
         val focusModifier2 = FocusModifier(Inactive)
         val focusModifier3 = FocusModifier(Inactive)
+        val focusModifier4 = FocusModifier(Inactive)
         // Arrange.
-        // layoutNode--focusNode1--focusNode2--focusNode3
+        // layoutNode--focusNode1--focusNode2--focusNode3--focusNode4
         rule.setContent {
-            Box(modifier = focusModifier1.then(focusModifier2).then(focusModifier3))
+            Box(
+                modifier = Modifier
+                .then(focusModifier1)
+                .focusProperties { canFocus = false }
+                .then(focusModifier2)
+                .then(focusModifier3)
+                .then(focusModifier4)
+            )
         }
 
         // Act.
         val focusableChildren = rule.runOnIdle {
-            focusModifier1.focusNode.focusableChildren()
+            focusModifier1.focusNode.focusableChildren(excludeDeactivated)
         }
 
         // Assert.
         rule.runOnIdle {
-            assertThat(focusableChildren).containsExactly(focusModifier2.focusNode)
+            if (excludeDeactivated) {
+                assertThat(focusableChildren).containsExactly(focusModifier3.focusNode)
+            } else {
+                assertThat(focusableChildren).containsExactly(focusModifier2.focusNode)
+            }
         }
     }
 
@@ -60,20 +79,32 @@
     fun skipsNonFocusNodesAndReturnsFirstFocusNodeInModifierChain() {
         val focusModifier1 = FocusModifier(Inactive)
         val focusModifier2 = FocusModifier(Inactive)
+        val focusModifier3 = FocusModifier(Inactive)
         // Arrange.
-        // layoutNode--focusNode1--nonFocusNode--focusNode2
+        // layoutNode--focusNode1--nonFocusNode--focusNode2--focusNode3
         rule.setContent {
-            Box(focusModifier1.background(color = Red).then(focusModifier2))
+            Box(
+                modifier = Modifier
+                    .then(focusModifier1)
+                    .background(color = Red)
+                    .focusProperties { canFocus = false }
+                    .then(focusModifier2)
+                    .then(focusModifier3)
+            )
         }
 
         // Act.
         val focusableChildren = rule.runOnIdle {
-            focusModifier1.focusNode.focusableChildren()
+            focusModifier1.focusNode.focusableChildren(excludeDeactivated)
         }
 
         // Assert.
         rule.runOnIdle {
-            assertThat(focusableChildren).containsExactly(focusModifier2.focusNode)
+            if (excludeDeactivated) {
+                assertThat(focusableChildren).containsExactly(focusModifier3.focusNode)
+            } else {
+                assertThat(focusableChildren).containsExactly(focusModifier2.focusNode)
+            }
         }
     }
 
@@ -81,30 +112,45 @@
     fun returnsFirstFocusChildOfEachChildLayoutNode() {
         // Arrange.
         // parentLayoutNode--parentFocusNode
-        //       |___________________________________
-        //       |                                   |
-        // childLayoutNode1--focusNode1          childLayoutNode2--focusNode2--focusNode3
+        //       |___________________________________________
+        //       |                                          |
+        // childLayoutNode1--focusNode1--focusNode2    childLayoutNode2--focusNode3--focusNode4
         val parentFocusModifier = FocusModifier(Inactive)
         val focusModifier1 = FocusModifier(Inactive)
         val focusModifier2 = FocusModifier(Inactive)
         val focusModifier3 = FocusModifier(Inactive)
+        val focusModifier4 = FocusModifier(Inactive)
         rule.setContent {
             Box(modifier = parentFocusModifier) {
-                Box(modifier = focusModifier1)
-                Box(modifier = focusModifier2.then(focusModifier3))
+                Box(modifier = Modifier
+                    .focusProperties { canFocus = false }
+                    .then(focusModifier1)
+                    .then(focusModifier2)
+                )
+                Box(modifier = Modifier
+                    .then(focusModifier3)
+                    .focusProperties { canFocus = false }
+                    .then(focusModifier4)
+                )
             }
         }
 
         // Act.
         val focusableChildren = rule.runOnIdle {
-            parentFocusModifier.focusNode.focusableChildren()
+            parentFocusModifier.focusNode.focusableChildren(excludeDeactivated)
         }
 
         // Assert.
         rule.runOnIdle {
-            assertThat(focusableChildren).containsExactly(
-                focusModifier1.focusNode, focusModifier2.focusNode
-            )
+            if (excludeDeactivated) {
+                assertThat(focusableChildren).containsExactly(
+                    focusModifier2.focusNode, focusModifier3.focusNode
+                )
+            } else {
+                assertThat(focusableChildren).containsExactly(
+                    focusModifier1.focusNode, focusModifier3.focusNode
+                )
+            }
         }
     }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
index ec506c2..494246c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
@@ -18,22 +18,29 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.graphics.Color.Companion.Red
 import androidx.compose.ui.test.junit4.createComposeRule
-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
+import org.junit.runners.Parameterized
 
 @MediumTest
-@RunWith(AndroidJUnit4::class)
-class FindParentFocusNodeTest {
+@RunWith(Parameterized::class)
+class FindParentFocusNodeTest(private val deactivated: Boolean) {
     @get:Rule
     val rule = createComposeRule()
 
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "isDeactivated = {0}")
+        fun initParameters() = listOf(true, false)
+    }
+
     @Test
     fun noParentReturnsNull() {
         // Arrange.
@@ -63,7 +70,13 @@
         val modifier4 = FocusModifier(Inactive)
         val modifier5 = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier1.then(modifier2).then(modifier3).then(modifier4).then(modifier5)) {}
+            Box(modifier = modifier1
+                .focusProperties { canFocus = !deactivated }
+                .then(modifier2)
+                .then(modifier3)
+                .then(modifier4)
+                .then(modifier5)
+            )
         }
 
         // Act.
@@ -87,6 +100,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = modifier1
+                    .focusProperties { canFocus = !deactivated }
                     .then(modifier2)
                     .background(color = Red)
                     .then(modifier3)
@@ -114,7 +128,10 @@
         val parentFocusModifier2 = FocusModifier(Inactive)
         val focusModifier = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier = parentFocusModifier1.then(parentFocusModifier2)) {
+            Box(modifier = parentFocusModifier1
+                .focusProperties { canFocus = !deactivated }
+                .then(parentFocusModifier2)
+            ) {
                 Box(modifier = focusModifier)
             }
         }
@@ -133,18 +150,26 @@
     @Test
     fun returnsImmediateParent() {
         // Arrange.
+        // greatGrandparentLayoutNode--greatGrandparentFocusNode
+        //       |
         // grandparentLayoutNode--grandparentFocusNode
         //       |
         // parentLayoutNode--parentFocusNode
         //       |
         // layoutNode--focusNode
+        val greatGrandparentFocusModifier = FocusModifier(Inactive)
         val grandparentFocusModifier = FocusModifier(Inactive)
         val parentFocusModifier = FocusModifier(Inactive)
         val focusModifier = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier = grandparentFocusModifier) {
-                Box(modifier = parentFocusModifier) {
-                    Box(modifier = focusModifier)
+            Box(modifier = greatGrandparentFocusModifier) {
+                Box(modifier = grandparentFocusModifier) {
+                    Box(modifier = Modifier
+                        .focusProperties { canFocus = !deactivated }
+                        .then(parentFocusModifier)
+                    ) {
+                        Box(modifier = focusModifier)
+                    }
                 }
             }
         }
@@ -161,19 +186,25 @@
     }
 
     @Test
-    fun ignoresIntermediateLayoutNodesThatDontHaveFocusNodes() {
+    fun ignoresIntermediateLayoutNodesThatDoNotHaveFocusNodes() {
         // Arrange.
         // grandparentLayoutNode--grandparentFocusNode
         //       |
         // parentLayoutNode
         //       |
         // layoutNode--focusNode
+        val greatGrandparentFocusModifier = FocusModifier(Inactive)
         val grandparentFocusModifier = FocusModifier(Inactive)
         val focusModifier = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier = grandparentFocusModifier) {
-                Box {
-                    Box(modifier = focusModifier)
+            Box(modifier = greatGrandparentFocusModifier) {
+                Box(modifier = Modifier
+                    .focusProperties { canFocus = !deactivated }
+                    .then(grandparentFocusModifier)
+                ) {
+                    Box {
+                        Box(modifier = focusModifier)
+                    }
                 }
             }
         }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
index 781fa34..ec729ec 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -65,6 +64,7 @@
     fun activeParent_requestFocus() {
         // Arrange.
         lateinit var focusState: FocusState
+        lateinit var childFocusState: FocusState
         val (focusRequester, childFocusRequester) = FocusRequester.createRefs()
         rule.setFocusableContent {
             Box(
@@ -75,6 +75,7 @@
             ) {
                 Box(
                     modifier = Modifier
+                        .onFocusChanged { childFocusState = it }
                         .focusRequester(childFocusRequester)
                         .focusTarget()
                 )
@@ -91,6 +92,7 @@
 
             // Assert.
             assertThat(focusState.isFocused).isTrue()
+            assertThat(childFocusState.isFocused).isFalse()
         }
     }
 
@@ -118,7 +120,7 @@
     }
 
     @Test
-    fun disabled_requestFocus() {
+    fun deactivated_requestFocus() {
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
@@ -127,7 +129,8 @@
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(FocusModifier(Disabled))
+                    .focusProperties { canFocus = false }
+                    .focusTarget()
             )
         }
 
@@ -136,7 +139,50 @@
             focusRequester.requestFocus()
 
             // Assert.
-            assertThat(focusState).isEqualTo(Disabled)
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @ExperimentalComposeUiApi
+    @Test
+    fun deactivatedParent_requestFocus() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        lateinit var childFocusState: FocusState
+        val (focusRequester, childFocusRequester) = FocusRequester.createRefs()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .focusRequester(focusRequester)
+                    .focusProperties { canFocus = false }
+                    .focusTarget()
+            ) {
+                Box(
+                    modifier = Modifier
+                        .onFocusChanged { childFocusState = it }
+                        .focusRequester(childFocusRequester)
+                        .focusTarget()
+                )
+            }
+        }
+        rule.runOnIdle {
+            childFocusRequester.requestFocus()
+            assertThat(childFocusState.isFocused).isTrue()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+
+        rule.runOnIdle {
+            // Act.
+            focusRequester.requestFocus()
+
+            // Assert.
+            assertThat(childFocusState.isFocused).isTrue()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
         }
     }
 
@@ -244,3 +290,6 @@
         }
     }
 }
+
+private val FocusState.isDeactivated: Boolean
+    get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
index dcafd34..af002fb 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
@@ -17,12 +17,13 @@
 package androidx.compose.ui.focus
 
 import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.MutableState
+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.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,12 +43,12 @@
     fun initially_onFocusEventIsCalledThrice() {
         // Arrange.
         val focusStates = mutableListOf<FocusState>()
-        val focusReferece = FocusRequester()
+        val focusRequester = FocusRequester()
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
                     .onFocusEvent { focusStates.add(it) }
-                    .focusRequester(focusReferece)
+                    .focusRequester(focusRequester)
                     .focusTarget()
             )
         }
@@ -56,7 +57,6 @@
         rule.runOnIdle {
             assertThat(focusStates).containsExactly(
                 Inactive, // triggered by onFocusEvent node's onModifierChanged().
-                Inactive, // triggered by focus node's onModifierChanged().
                 Inactive, // triggered by focus node's attach().
             )
         }
@@ -153,14 +153,13 @@
         // Arrange.
         val focusStates = mutableListOf<FocusState>()
         val focusRequester = FocusRequester()
-        lateinit var addFocusTarget: MutableState<Boolean>
+        var addFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            addFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier
                     .onFocusEvent { focusStates.add(it) }
                     .focusRequester(focusRequester)
-                    .then(if (addFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
             )
         }
         rule.runOnIdle {
@@ -169,7 +168,7 @@
         }
 
         // Act.
-        rule.runOnIdle { addFocusTarget.value = false }
+        rule.runOnIdle { addFocusTarget = false }
 
         // Assert.
         rule.runOnIdle {
@@ -184,19 +183,18 @@
     fun removingInactiveFocusNode_onFocusEventIsCalledOnce() {
         // Arrange.
         val focusStates = mutableListOf<FocusState>()
-        lateinit var addFocusTarget: MutableState<Boolean>
+        var addFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            addFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier
                     .onFocusEvent { focusStates.add(it) }
-                    .then(if (addFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
             )
         }
         rule.runOnIdle { focusStates.clear() }
 
         // Act.
-        rule.runOnIdle { addFocusTarget.value = false }
+        rule.runOnIdle { addFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
@@ -206,19 +204,18 @@
     fun addingFocusTarget_onFocusEventIsCalledThrice() {
         // Arrange.
         val focusStates = mutableListOf<FocusState>()
-        lateinit var addFocusTarget: MutableState<Boolean>
+        var addFocusTarget by mutableStateOf(false)
         rule.setFocusableContent {
-            addFocusTarget = remember { mutableStateOf(false) }
             Box(
                 modifier = Modifier
                     .onFocusEvent { focusStates.add(it) }
-                    .then(if (addFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
             )
         }
         rule.runOnIdle { focusStates.clear() }
 
         // Act.
-        rule.runOnIdle { addFocusTarget.value = true }
+        rule.runOnIdle { addFocusTarget = true }
 
         // Assert.
         rule.runOnIdle {
@@ -229,4 +226,75 @@
             )
         }
     }
+
+    @Test
+    fun addingEmptyFocusProperties_onFocusEventIsCalledTwice() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        var addFocusProperties by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .then(if (addFocusProperties) Modifier.focusProperties {} else Modifier)
+                    .focusTarget()
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { addFocusProperties = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusStates).containsExactly(
+                Inactive, // triggered by onFocusEvent node's onModifierChanged().
+                Inactive, // triggered by focus node's onModifierChanged().
+            )
+        }
+    }
+
+    @Test
+    fun deactivatingFocusNode_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        var deactiated by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusProperties { canFocus = !deactiated }
+                    .focusTarget()
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { deactiated = true }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Deactivated) }
+    }
+
+    @Test
+    fun activatingFocusNode_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        var deactiated by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusProperties { canFocus = !deactiated }
+                    .focusTarget()
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { deactiated = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
index 6939cc9..f03c428 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
@@ -17,9 +17,9 @@
 package androidx.compose.ui.focus
 
 import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.MutableState
+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.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -40,16 +40,15 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var observingFocusTarget1: MutableState<Boolean>
+        var observingFocusTarget1 by mutableStateOf(true)
         rule.setFocusableContent {
             val focusRequesterModifier = Modifier.focusRequester(focusRequester)
             val onFocusChanged = Modifier.onFocusChanged { focusState = it }
             val focusTarget1 = Modifier.focusTarget()
             val focusTarget2 = Modifier.focusTarget()
             Box {
-                observingFocusTarget1 = remember { mutableStateOf(true) }
                 Box(
-                    modifier = if (observingFocusTarget1.value) {
+                    modifier = if (observingFocusTarget1) {
                         onFocusChanged
                             .then(focusRequesterModifier)
                             .then(focusTarget1)
@@ -69,7 +68,7 @@
         }
 
         // Act.
-        rule.runOnIdle { observingFocusTarget1.value = false }
+        rule.runOnIdle { observingFocusTarget1 = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -80,15 +79,14 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var onFocusChangedHasFocusTarget: MutableState<Boolean>
+        var onFocusChangedHasFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
             val focusRequesterModifier = Modifier.focusRequester(focusRequester)
             val onFocusChanged = Modifier.onFocusChanged { focusState = it }
             val focusTarget = Modifier.focusTarget()
             Box {
-                onFocusChangedHasFocusTarget = remember { mutableStateOf(true) }
                 Box(
-                    modifier = if (onFocusChangedHasFocusTarget.value) {
+                    modifier = if (onFocusChangedHasFocusTarget) {
                         onFocusChanged
                             .then(focusRequesterModifier)
                             .then(focusTarget)
@@ -106,7 +104,7 @@
         }
 
         // Act.
-        rule.runOnIdle { onFocusChangedHasFocusTarget.value = false }
+        rule.runOnIdle { onFocusChangedHasFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -117,13 +115,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(if (optionalFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
             )
         }
         rule.runOnIdle {
@@ -132,7 +129,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -143,13 +140,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(if (optionalFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
             ) {
                 Box(modifier = Modifier.focusTarget())
             }
@@ -160,7 +156,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -171,13 +167,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(if (optionalFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
             ) {
                 Box(modifier = Modifier.focusTarget())
             }
@@ -189,7 +184,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -200,15 +195,17 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.onFocusChanged { focusState = it }
-                    .then(if (optionalFocusTarget.value) Modifier.focusTarget() else Modifier)
-                    .focusRequester(focusRequester)
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
             ) {
-                Box(modifier = Modifier.focusTarget())
+                Box(modifier = Modifier
+                    .focusRequester(focusRequester)
+                    .focusTarget()
+                )
             }
         }
         rule.runOnIdle {
@@ -217,7 +214,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isTrue() }
@@ -228,13 +225,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .then(
-                        if (optionalFocusTarget.value) {
+                        if (optionalFocusTarget) {
                             Modifier
                                 .focusTarget()
                                 .focusRequester(focusRequester)
@@ -251,7 +247,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -263,9 +259,8 @@
         lateinit var focusState: FocusState
         lateinit var parentFocusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTargets: MutableState<Boolean>
+        var optionalFocusTargets by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTargets = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier
                     .onFocusChanged { parentFocusState = it }
@@ -273,7 +268,7 @@
             ) {
                 Box(
                     modifier = Modifier.onFocusChanged { focusState = it }.then(
-                        if (optionalFocusTargets.value) {
+                        if (optionalFocusTargets) {
                             Modifier.focusTarget()
                                 .focusRequester(focusRequester)
                                 .focusTarget()
@@ -291,7 +286,7 @@
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTargets.value = false }
+        rule.runOnIdle { optionalFocusTargets = false }
 
         // Assert.
         rule.runOnIdle {
@@ -301,23 +296,269 @@
     }
 
     @Test
+    fun removedDeactivatedParentFocusTarget_pointsToNextFocusTarget() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalFocusTarget by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(
+                        if (optionalFocusTarget)
+                            Modifier
+                                .focusProperties { canFocus = false }
+                                .focusTarget()
+                        else
+                            Modifier
+                    )
+            ) {
+                Box(modifier = Modifier
+                    .focusRequester(focusRequester)
+                    .focusTarget()
+                )
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalFocusTarget = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isTrue()
+            assertThat(focusState.isDeactivated).isFalse()
+        }
+    }
+
+    @Test
+    fun removedDeactivatedParentFocusTarget_pointsToNextDeactivatedParentFocusTarget() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalFocusTarget by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(
+                        if (optionalFocusTarget)
+                            Modifier
+                                .focusProperties { canFocus = false }
+                                .focusTarget()
+                        else
+                            Modifier
+                    )
+            ) {
+                Box(
+                    modifier = Modifier
+                        .onFocusChanged { focusState = it }
+                        .focusProperties { canFocus = false }
+                        .focusTarget()
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .focusRequester(focusRequester)
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalFocusTarget = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun removedDeactivatedParent_parentsFocusTarget_isUnchanged() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalFocusTarget by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .focusProperties { canFocus = false }
+                    .focusTarget()
+            ) {
+                Box(
+                    modifier = Modifier.then(
+                            if (optionalFocusTarget)
+                                Modifier
+                                    .focusProperties { canFocus = false }
+                                    .focusTarget()
+                            else
+                                Modifier
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .focusRequester(focusRequester)
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalFocusTarget = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun removedDeactivatedParentAndActiveChild_grandparent_retainsDeactivatedState() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalFocusTarget by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .focusProperties { canFocus = false }
+                    .focusTarget()
+            ) {
+                Box(
+                    modifier = Modifier
+                        .onFocusChanged { focusState = it }
+                        .then(
+                            if (optionalFocusTarget)
+                                Modifier
+                                    .focusProperties { canFocus = false }
+                                    .focusTarget()
+                            else
+                                Modifier
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .focusRequester(focusRequester)
+                            .then(
+                                if (optionalFocusTarget)
+                                    Modifier.focusTarget()
+                                else
+                                    Modifier
+                            )
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalFocusTarget = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.hasFocus).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
+
+    @Test
+    fun removedNonDeactivatedParentAndActiveChild_grandParent_retainsNonDeactivatedState() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalFocusTarget by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .focusTarget()
+            ) {
+                Box(
+                    modifier = Modifier.then(
+                            if (optionalFocusTarget)
+                                Modifier
+                                    .focusProperties { canFocus = false }
+                                    .focusTarget()
+                            else
+                                Modifier
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .focusRequester(focusRequester)
+                            .then(
+                                if (optionalFocusTarget)
+                                    Modifier.focusTarget()
+                                else
+                                    Modifier
+                            )
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(focusState.isDeactivated).isFalse()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalFocusTarget = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.hasFocus).isFalse()
+            assertThat(focusState.isDeactivated).isFalse()
+        }
+    }
+
+    @Test
     fun removedInactiveFocusTarget_pointsToNextFocusTarget() {
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var optionalFocusTarget: MutableState<Boolean>
+        var optionalFocusTarget by mutableStateOf(true)
         rule.setFocusableContent {
-            optionalFocusTarget = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
-                    .then(if (optionalFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
                     .focusRequester(focusRequester)
                     .focusTarget()
             )
         }
 
         // Act.
-        rule.runOnIdle { optionalFocusTarget.value = false }
+        rule.runOnIdle { optionalFocusTarget = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -328,13 +569,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var addFocusTarget: MutableState<Boolean>
+        var addFocusTarget by mutableStateOf(false)
         rule.setFocusableContent {
-            addFocusTarget = remember { mutableStateOf(false) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(if (addFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
             ) {
                 Box(modifier = Modifier.focusTarget())
             }
@@ -345,7 +585,7 @@
         }
 
         // Act.
-        rule.runOnIdle { addFocusTarget.value = true }
+        rule.runOnIdle { addFocusTarget = true }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
@@ -356,13 +596,12 @@
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
-        lateinit var addFocusTarget: MutableState<Boolean>
+        var addFocusTarget by mutableStateOf(false)
         rule.setFocusableContent {
-            addFocusTarget = remember { mutableStateOf(false) }
             Box(
                 modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(if (addFocusTarget.value) Modifier.focusTarget() else Modifier)
+                    .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
             )
         }
         rule.runOnIdle {
@@ -371,9 +610,109 @@
         }
 
         // Act.
-        rule.runOnIdle { addFocusTarget.value = true }
+        rule.runOnIdle { addFocusTarget = true }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
     }
+
+    @Test
+    fun removingDeactivatedItem_withNoNextFocusTarget() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        var removeDeactivatedItem by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(
+                        if (removeDeactivatedItem)
+                            Modifier
+                        else
+                            Modifier
+                                .focusProperties { canFocus = false }
+                                .focusTarget()
+                    )
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { removeDeactivatedItem = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isFalse()
+        }
+    }
+
+    @Test
+    fun removingDeactivatedItem_withInactiveNextFocusTarget() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        var removeDeactivatedItem by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(
+                        if (removeDeactivatedItem)
+                            Modifier
+                        else
+                            Modifier
+                                .focusProperties { canFocus = false }
+                                .focusTarget()
+                    )
+            ) {
+                Box(modifier = Modifier.focusTarget())
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { removeDeactivatedItem = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isFalse()
+        }
+    }
+
+    @Test
+    fun removingDeactivatedItem_withDeactivatedNextFocusTarget() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        var removeDeactivatedItem by mutableStateOf(false)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusState = it }
+                    .then(
+                        if (removeDeactivatedItem)
+                            Modifier
+                        else
+                            Modifier
+                                .focusProperties { canFocus = false }
+                                .focusTarget()
+                    )
+            ) {
+                Box(modifier = Modifier
+                    .focusProperties { canFocus = false }
+                    .focusTarget()
+                )
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { removeDeactivatedItem = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
+        }
+    }
 }
+
+private val FocusState.isDeactivated: Boolean
+    get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
index 80e2b01..1094abc 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
@@ -51,6 +51,7 @@
     width: Int,
     height: Int,
     focusRequester: FocusRequester? = null,
+    deactivated: Boolean = false,
     content: @Composable () -> Unit = {}
 ) {
     Layout(
@@ -59,6 +60,7 @@
             .offset { IntOffset(x, y) }
             .focusRequester(focusRequester ?: remember { FocusRequester() })
             .onFocusChanged { isFocused.value = it.isFocused }
+            .focusProperties { canFocus = !deactivated }
             .focusTarget(),
         measurePolicy = remember(width, height) {
             MeasurePolicy { measurables, constraint ->
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
index 37eefcd..a0845c9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
@@ -21,12 +21,11 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -56,8 +55,8 @@
             val success = focusRequester.freeFocus()
 
             // Assert.
-            Truth.assertThat(success).isTrue()
-            Truth.assertThat(focusState.isFocused).isTrue()
+            assertThat(success).isTrue()
+            assertThat(focusState.isFocused).isTrue()
         }
     }
 
@@ -80,8 +79,8 @@
             val success = focusRequester.freeFocus()
 
             // Assert.
-            Truth.assertThat(success).isFalse()
-            Truth.assertThat(focusState.hasFocus).isTrue()
+            assertThat(success).isFalse()
+            assertThat(focusState.hasFocus).isTrue()
         }
     }
 
@@ -104,13 +103,13 @@
             val success = focusRequester.freeFocus()
 
             // Assert.
-            Truth.assertThat(success).isTrue()
-            Truth.assertThat(focusState.isFocused).isTrue()
+            assertThat(success).isTrue()
+            assertThat(focusState.isFocused).isTrue()
         }
     }
 
     @Test
-    fun disabled_freeFocus_retainFocusAsDisabled() {
+    fun deactivated_freeFocus_retainFocusAsDeactivated() {
         // Arrange.
         lateinit var focusState: FocusState
         val focusRequester = FocusRequester()
@@ -119,7 +118,8 @@
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
-                    .then(FocusModifier(Disabled))
+                    .focusProperties { canFocus = false }
+                    .then(FocusModifier(Inactive))
             )
         }
 
@@ -128,8 +128,8 @@
             val success = focusRequester.freeFocus()
 
             // Assert.
-            Truth.assertThat(success).isFalse()
-            Truth.assertThat(focusState).isEqualTo(Disabled)
+            assertThat(success).isFalse()
+            assertThat(focusState.isDeactivated).isTrue()
         }
     }
 
@@ -152,8 +152,11 @@
             val success = focusRequester.freeFocus()
 
             // Assert.
-            Truth.assertThat(success).isFalse()
-            Truth.assertThat(focusState.isFocused).isFalse()
+            assertThat(success).isFalse()
+            assertThat(focusState.isFocused).isFalse()
         }
     }
 }
+
+private val FocusState.isDeactivated: Boolean
+    get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
index 2288a5b..4140f6b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.focus
 
+import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.focus.FocusDirection.Companion.Next
@@ -45,7 +46,22 @@
         rule.setContentWithInitialRootFocus {}
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle { assertThat(movedFocusSuccessfully).isFalse() }
+    }
+
+    @Test
+    fun moveFocus_oneDisabledFocusableItem() {
+        // Arrange.
+        val isItemFocused = mutableStateOf(false)
+        rule.setContentWithInitialRootFocus {
+            FocusableBox(isItemFocused, 0, 0, 10, 10, deactivated = true)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle { assertThat(movedFocusSuccessfully).isFalse() }
@@ -60,7 +76,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -70,6 +86,28 @@
     }
 
     @Test
+    fun initialFocus_skipsDeactivatedItem() {
+        // Arrange.
+        val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
+        rule.setContentWithInitialRootFocus {
+            Column {
+                FocusableBox(firstItem, 0, 0, 10, 10, deactivated = true)
+                FocusableBox(secondItem, 0, 0, 10, 10)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(firstItem.value).isFalse()
+            assertThat(secondItem.value).isTrue()
+        }
+    }
+
+    @Test
     fun initialFocus_firstItemInCompositionOrderGetsFocus() {
         // Arrange.
         val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
@@ -79,7 +117,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -102,7 +140,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -123,7 +161,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -144,7 +182,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -164,7 +202,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -174,6 +212,27 @@
     }
 
     @Test
+    fun focusMovesToThirdItem_skipsDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+            FocusableBox(item2, 10, 0, 10, 10, deactivated = true)
+            FocusableBox(item3, 10, 0, 10, 10)
+            FocusableBox(item4, 20, 0, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item3.value).isTrue()
+        }
+    }
+
+    @Test
     fun focusMovesToThirdItem() {
         // Arrange.
         val (item1, item2, item3) = List(3) { mutableStateOf(false) }
@@ -184,7 +243,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -194,6 +253,27 @@
     }
 
     @Test
+    fun focusMovesToFourthItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10)
+            FocusableBox(item2, 0, 0, 10, 10, deactivated = true)
+            FocusableBox(item3, 10, 0, 10, 10, initialFocus)
+            FocusableBox(item4, 20, 0, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item4.value).isTrue()
+        }
+    }
+
+    @Test
     fun focusWrapsAroundToFirstItem() {
         // Arrange.
         val (item1, item2, item3) = List(3) { mutableStateOf(false) }
@@ -204,7 +284,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Next)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
 
         // Assert.
         rule.runOnIdle {
@@ -214,11 +294,55 @@
     }
 
     @Test
+    fun focusWrapsAroundToFirstItem_skippingLastDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10)
+            FocusableBox(item2, 10, 0, 10, 10)
+            FocusableBox(item3, 20, 0, 10, 10, initialFocus)
+            FocusableBox(item4, 10, 0, 10, 10, deactivated = true)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item1.value).isTrue()
+        }
+    }
+
+    @Test
+    fun focusWrapsAroundToFirstItem_skippingFirstDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 10, 0, 10, 10, deactivated = true)
+            FocusableBox(item2, 0, 0, 10, 10)
+            FocusableBox(item3, 10, 0, 10, 10)
+            FocusableBox(item4, 20, 0, 10, 10, initialFocus)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item2.value).isTrue()
+        }
+    }
+
+    @Test
     fun focusNextOrdering() {
         // Arrange.
         val (parent1, child1, child2, child3) = List(4) { mutableStateOf(false) }
         val (parent2, child4, child5) = List(3) { mutableStateOf(false) }
         val (parent3, child6) = List(2) { mutableStateOf(false) }
+        val (parent4, child7, child8, child9, child10) = List(5) { mutableStateOf(false) }
+        val (parent5, child11) = List(2) { mutableStateOf(false) }
         rule.setContentWithInitialRootFocus {
             FocusableBox(parent1, 0, 0, 10, 10) {
                 FocusableBox(child1, 0, 0, 10, 10)
@@ -232,37 +356,55 @@
                 }
                 FocusableBox(child5, 20, 0, 10, 10)
             }
+            FocusableBox(parent4, 0, 10, 10, 10, deactivated = true) {
+                FocusableBox(child7, 0, 10, 10, 10, deactivated = true)
+                FocusableBox(child8, 0, 10, 10, 10)
+                FocusableBox(parent5, 10, 10, 10, 10, deactivated = true) {
+                    FocusableBox(child11, 0, 0, 10, 10)
+                }
+                FocusableBox(child9, 20, 0, 10, 10)
+                FocusableBox(child10, 20, 0, 10, 10, deactivated = true)
+            }
         }
 
         // Act & Assert.
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(parent1.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child1.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child2.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child3.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(parent2.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child4.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(parent3.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child6.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(child5.value).isTrue() }
 
-        focusManager.moveFocus(Next)
+        rule.runOnIdle { focusManager.moveFocus(Next) }
+        rule.runOnIdle { assertThat(child8.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Next) }
+        rule.runOnIdle { assertThat(child11.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Next) }
+        rule.runOnIdle { assertThat(child9.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Next) }
         rule.runOnIdle { assertThat(parent1.value).isTrue() }
     }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
index dc09292..115d70e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.focus
 
+import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
@@ -45,7 +46,22 @@
         rule.setContentWithInitialRootFocus {}
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle { assertThat(movedFocusSuccessfully).isFalse() }
+    }
+
+    @Test
+    fun moveFocus_oneDisabledFocusableItem() {
+        // Arrange.
+        val isItemFocused = mutableStateOf(false)
+        rule.setContentWithInitialRootFocus {
+            FocusableBox(isItemFocused, 0, 0, 10, 10, deactivated = true)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle { assertThat(movedFocusSuccessfully).isFalse() }
@@ -60,7 +76,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -70,6 +86,28 @@
     }
 
     @Test
+    fun initialFocus_skipsDeactivatedItem() {
+        // Arrange.
+        val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
+        rule.setContentWithInitialRootFocus {
+            Column {
+                FocusableBox(firstItem, 0, 0, 10, 10)
+                FocusableBox(secondItem, 0, 0, 10, 10, deactivated = true)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(firstItem.value).isTrue()
+            assertThat(secondItem.value).isFalse()
+        }
+    }
+
+    @Test
     fun initialFocus_lastItemInCompositionOrderGetsFocus() {
         // Arrange.
         val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
@@ -79,7 +117,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -102,7 +140,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -123,7 +161,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -144,7 +182,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -164,7 +202,28 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item2.value).isTrue()
+        }
+    }
+
+    @Test
+    fun focusMovesToSecondItem_skipsDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10)
+            FocusableBox(item2, 10, 0, 10, 10)
+            FocusableBox(item3, 10, 0, 10, 10, deactivated = true)
+            FocusableBox(item4, 20, 0, 10, 10, initialFocus)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -184,7 +243,28 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item1.value).isTrue()
+        }
+    }
+
+    @Test
+    fun focusMovesToFirstItem_ignoresDeactivated() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10)
+            FocusableBox(item2, 10, 0, 10, 10, initialFocus)
+            FocusableBox(item3, 20, 0, 10, 10, deactivated = true)
+            FocusableBox(item4, 20, 0, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -204,7 +284,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Previous)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
 
         // Assert.
         rule.runOnIdle {
@@ -214,11 +294,56 @@
     }
 
     @Test
-    fun focusNextOrdering() {
+    fun focusWrapsAroundToLastItem_skippingFirstDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 20, 0, 10, 10, deactivated = true)
+            FocusableBox(item2, 0, 0, 10, 10, initialFocus)
+            FocusableBox(item3, 10, 0, 10, 10)
+            FocusableBox(item4, 20, 0, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item4.value).isTrue()
+        }
+    }
+
+    @Test
+    fun focusWrapsAroundToLastItem_skippingLastDeactivatedItem() {
+        // Arrange.
+        val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 0, 10, 10, initialFocus)
+            FocusableBox(item2, 10, 0, 10, 10)
+            FocusableBox(item3, 20, 0, 10, 10)
+            FocusableBox(item4, 20, 0, 10, 10, deactivated = true)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(item3.value).isTrue()
+        }
+    }
+
+    @Test
+    fun focusPreviousOrdering() {
         // Arrange.
         val (parent1, child1, child2, child3) = List(4) { mutableStateOf(false) }
         val (parent2, child4, child5) = List(3) { mutableStateOf(false) }
         val (parent3, child6) = List(2) { mutableStateOf(false) }
+        val (parent4, child7, child8, child11, child12) = List(5) { mutableStateOf(false) }
+        val (child9, child10) = List(2) { mutableStateOf(false) }
+        val (parent5, child13) = List(2) { mutableStateOf(false) }
         rule.setContentWithInitialRootFocus {
             FocusableBox(parent1, 0, 0, 10, 10) {
                 FocusableBox(child1, 0, 0, 10, 10)
@@ -232,38 +357,61 @@
                 }
                 FocusableBox(child5, 20, 0, 10, 10)
             }
+            FocusableBox(parent4, 0, 10, 10, 10, deactivated = true) {
+                FocusableBox(child7, 0, 10, 10, 10, deactivated = true)
+                FocusableBox(child8, 0, 10, 10, 10)
+                FocusableBox(child9, 0, 10, 10, 10, deactivated = true)
+                FocusableBox(child10, 0, 10, 10, 10)
+                FocusableBox(parent5, 10, 10, 10, 10, deactivated = true) {
+                    FocusableBox(child13, 0, 0, 10, 10)
+                }
+                FocusableBox(child11, 20, 0, 10, 10)
+                FocusableBox(child12, 20, 0, 10, 10, deactivated = true)
+            }
         }
 
         // Act & Assert.
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
+        rule.runOnIdle { assertThat(child11.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
+        rule.runOnIdle { assertThat(child13.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
+        rule.runOnIdle { assertThat(child10.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
+        rule.runOnIdle { assertThat(child8.value).isTrue() }
+
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child5.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child6.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(parent3.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child4.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(parent2.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child3.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child2.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(child1.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
         rule.runOnIdle { assertThat(parent1.value).isTrue() }
 
-        focusManager.moveFocus(Previous)
-        rule.runOnIdle { assertThat(child5.value).isTrue() }
+        rule.runOnIdle { focusManager.moveFocus(Previous) }
+        rule.runOnIdle { assertThat(child11.value).isTrue() }
     }
 
     private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
index 5fda2c1..ad2151b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
@@ -17,10 +17,12 @@
 package androidx.compose.ui.focus
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.filters.SmallTest
@@ -82,11 +84,11 @@
     }
 
     @Test
-    fun disabled_isUnchanged() {
+    fun deactivated_isUnchanged() {
         // Arrange.
-        val focusModifier = FocusModifier(Disabled)
+        val focusModifier = FocusModifier(Inactive)
         rule.setFocusableContent {
-            Box(modifier = focusModifier)
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(focusModifier))
         }
 
         // Act.
@@ -96,7 +98,7 @@
 
         // Assert.
         rule.runOnIdle {
-            assertThat(focusModifier.focusState).isEqualTo(Disabled)
+            assertThat(focusModifier.focusState).isEqualTo(Deactivated)
         }
     }
 
@@ -150,6 +152,87 @@
         }
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun deactivatedParent_withNoFocusedChild_throwsException() {
+        // Arrange.
+        val focusModifier = FocusModifier(DeactivatedParent)
+        rule.setFocusableContent {
+            Box(modifier = focusModifier)
+        }
+
+        // Act.
+        rule.runOnIdle {
+            focusModifier.focusNode.requestFocus(propagateFocus)
+        }
+    }
+
+    @Test
+    fun deactivatedParent_propagateFocus() {
+        // Arrange.
+        val focusModifier = FocusModifier(ActiveParent)
+        val childFocusModifier = FocusModifier(Active)
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(focusModifier)) {
+                Box(modifier = childFocusModifier)
+            }
+        }
+        rule.runOnIdle {
+            focusModifier.focusedChild = childFocusModifier.focusNode
+        }
+
+        // Act.
+        rule.runOnIdle {
+            focusModifier.focusNode.requestFocus(propagateFocus)
+        }
+
+        // Assert.
+        rule.runOnIdle {
+           // Unchanged.
+           assertThat(focusModifier.focusState).isEqualTo(DeactivatedParent)
+           assertThat(childFocusModifier.focusState).isEqualTo(Active)
+        }
+    }
+
+    @Test
+    fun deactivatedParent_activeChild_propagateFocus() {
+        // Arrange.
+        val focusModifier = FocusModifier(ActiveParent)
+        val childFocusModifier = FocusModifier(Active)
+        val grandchildFocusModifier = FocusModifier(Inactive)
+        rule.setFocusableContent {
+            Box(modifier = Modifier.focusProperties { canFocus = false }.then(focusModifier)) {
+                Box(modifier = childFocusModifier) {
+                    Box(modifier = grandchildFocusModifier)
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusModifier.focusedChild = childFocusModifier.focusNode
+        }
+
+        // Act.
+        rule.runOnIdle {
+            focusModifier.focusNode.requestFocus(propagateFocus)
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            when (propagateFocus) {
+                true -> {
+                    assertThat(focusModifier.focusState).isEqualTo(DeactivatedParent)
+                    assertThat(childFocusModifier.focusState).isEqualTo(Active)
+                    assertThat(grandchildFocusModifier.focusState).isEqualTo(Inactive)
+                }
+                false -> {
+                    assertThat(focusModifier.focusState).isEqualTo(DeactivatedParent)
+                    assertThat(childFocusModifier.focusState).isEqualTo(Active)
+                    assertThat(childFocusModifier.focusedChild).isNull()
+                    assertThat(grandchildFocusModifier.focusState).isEqualTo(Inactive)
+                }
+            }
+        }
+    }
+
     @Test
     fun inactiveRoot_propagateFocusSendsRequestToOwner_systemCanGrantFocus() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInTest.kt
index bcfcd7c..5e15e48 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInTest.kt
@@ -57,7 +57,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(In)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
 
         // Assert.
         rule.runOnIdle {
@@ -68,6 +68,37 @@
     }
 
     /**
+     *      ___________________   ____________
+     *     |    focusedItem   |  |           |
+     *     |  ______________  |  |           |
+     *     | | deactivated |  |  | otherItem |
+     *     | |_____________|  |  |           |
+     *     |__________________|  |___________|
+     */
+    @Test
+    fun focusIn_deactivatedChild_doesNotMoveFocus() {
+        // Arrange.
+        val (focusedItem, deactivatedItem, otherItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 10, 10, initialFocus) {
+                FocusableBox(deactivatedItem, 10, 0, 10, 10, deactivated = true)
+            }
+            FocusableBox(otherItem, 10, 0, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isFalse()
+            assertThat(focusedItem.value).isTrue()
+            assertThat(deactivatedItem.value).isFalse()
+            assertThat(otherItem.value).isFalse()
+        }
+    }
+
+    /**
      *      _______________
      *     |  focusedItem |
      *     |   _________  |
@@ -86,7 +117,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(In)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
 
         // Assert.
         rule.runOnIdle {
@@ -120,7 +151,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(In)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
 
         // Assert.
         rule.runOnIdle {
@@ -131,6 +162,41 @@
         }
     }
 
+    /**
+     *      _________________________
+     *     |  focusedItem           |
+     *     |   ___________________  |
+     *     |  |  child           |  |
+     *     |  |   _____________  |  |
+     *     |  |  | grandchild |  |  |
+     *     |  |  |____________|  |  |
+     *     |  |__________________|  |
+     *     |________________________|
+     */
+    @Test
+    fun focusIn_skipsImmediateDeactivatedChild() {
+        // Arrange.
+        val (child, grandchild) = List(2) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus) {
+                FocusableBox(child, 10, 10, 10, 10, deactivated = true) {
+                    FocusableBox(grandchild, 10, 10, 10, 10)
+                }
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(child.value).isFalse()
+            assertThat(grandchild.value).isTrue()
+        }
+    }
+
     // TODO(b/176847718): After RTL support is added, add a similar test where the topRight child
     //  is focused.
     /**
@@ -160,7 +226,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(In)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
 
         // Assert.
         rule.runOnIdle {
@@ -170,6 +236,43 @@
         }
     }
 
+    /**
+     *      _______________________________________
+     *     |  focusedItem                         |
+     *     |   _________   _________   _________  |
+     *     |  | child1 |  | child2 |  | child3 |  |
+     *     |  |________|  |________|  |________|  |
+     *     |   _________   _________   _________  |
+     *     |  | child4 |  | child5 |  | child6 |  |
+     *     |  |________|  |________|  |________|  |
+     *     |______________________________________|
+     */
+    @Test
+    fun focusIn_deactivatedTopLeftChildIsSkipped() {
+        // Arrange.
+        val children = List(6) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 70, 50, initialFocus) {
+                FocusableBox(children[0], 10, 10, 10, 10, deactivated = true)
+                FocusableBox(children[1], 30, 10, 10, 10)
+                FocusableBox(children[2], 50, 10, 10, 10)
+                FocusableBox(children[3], 10, 30, 10, 10)
+                FocusableBox(children[4], 30, 30, 10, 10)
+                FocusableBox(children[5], 50, 30, 10, 10)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(In) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(children.values).containsExactly(false, true, false, false, false, false)
+        }
+    }
+
     private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
         setContent {
             focusManager = LocalFocusManager.current
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
index 8f7cd87..1831252 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
@@ -24,7 +24,6 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection.Companion.Down
 import androidx.compose.ui.focus.FocusDirection.Companion.Left
 import androidx.compose.ui.focus.FocusDirection.Companion.Right
@@ -84,7 +83,7 @@
         rule.runOnIdle { view.requestFocus() }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
 
         // Assert.
         rule.runOnIdle {
@@ -99,6 +98,66 @@
     }
 
     @Test
+    fun initialFocus_DeactivatedItemIsSkipped() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var focusManager: FocusManager
+        val isFocused = MutableList(9) { mutableStateOf(false) }
+        rule.setContent {
+            view = LocalView.current
+            focusManager = LocalFocusManager.current
+            Column {
+                Row {
+                    FocusableBox(isFocused[0], deactivated = true)
+                    FocusableBox(isFocused[1])
+                    FocusableBox(isFocused[2])
+                }
+                Row {
+                    FocusableBox(isFocused[3])
+                    FocusableBox(isFocused[4])
+                    FocusableBox(isFocused[5])
+                }
+                Row {
+                    FocusableBox(isFocused[6], deactivated = true)
+                    FocusableBox(isFocused[7])
+                    FocusableBox(isFocused[8])
+                }
+            }
+        }
+        rule.runOnIdle { view.requestFocus() }
+
+        // Act.
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> assertThat(isFocused.values).containsExactly(
+                    false, false, false,
+                    false, false, false,
+                    false, true, false
+                )
+                Down -> assertThat(isFocused.values).containsExactly(
+                    false, true, false,
+                    false, false, false,
+                    false, false, false
+                )
+                Left -> assertThat(isFocused.values).containsExactly(
+                    false, true, false,
+                    false, false, false,
+                    false, false, false
+                )
+                Right -> assertThat(isFocused.values).containsExactly(
+                    false, true, false,
+                    false, false, false,
+                    false, false, false
+                )
+                else -> error(invalid)
+            }
+        }
+    }
+
+    @Test
     fun initialFocus_whenThereIsOnlyOneFocusable() {
         // Arrange.
         val isFocused = mutableStateOf(false)
@@ -112,7 +171,7 @@
         rule.runOnIdle { view.requestFocus() }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
 
         // Assert.
         rule.runOnIdle { assertThat(isFocused.value).isTrue() }
@@ -131,44 +190,23 @@
         rule.runOnIdle { view.requestFocus() }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
     }
 
     @Test
-    fun initialFocus_notTriggeredIfActiveElementIsNotRoot() {
+    fun doesNotCrash_whenThereIsOneDeactivatedItem() {
         // Arrange.
+        lateinit var view: View
         lateinit var focusManager: FocusManager
-        var isColumnFocused = false
-        val isFocused = MutableList(4) { mutableStateOf(false) }
-        val initialFocusRequester = FocusRequester()
         rule.setContent {
+            view = LocalView.current
             focusManager = LocalFocusManager.current
-            Column(
-                Modifier
-                    .focusRequester(initialFocusRequester)
-                    .onFocusChanged { isColumnFocused = it.isFocused }
-                    .focusTarget()
-            ) {
-                Row {
-                    FocusableBox(isFocused[0])
-                    FocusableBox(isFocused[1])
-                }
-                Row {
-                    FocusableBox(isFocused[2])
-                    FocusableBox(isFocused[3])
-                }
-            }
+            FocusableBox(deactivated = true)
         }
-        rule.runOnIdle { initialFocusRequester.requestFocus() }
+        rule.runOnIdle { view.requestFocus() }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
-
-        // Assert.
-        rule.runOnIdle {
-            assertThat(isColumnFocused).isTrue()
-            assertThat(isFocused.values).containsExactly(false, false, false, false)
-        }
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
     }
 
     @OptIn(ExperimentalComposeUiApi::class)
@@ -210,7 +248,7 @@
         rule.runOnIdle { initialFocusedItem.requestFocus() }
 
         // Act.
-        focusManager.moveFocus(focusDirection)
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
 
         // Assert.
         rule.runOnIdle {
@@ -221,15 +259,68 @@
             }
         }
     }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun movesFocusAmongSiblingsDeepInTheFocusHierarchy_skipsDeactivatedSibling() {
+        // Arrange.
+        lateinit var focusManager: FocusManager
+        val isFocused = MutableList(3) { mutableStateOf(false) }
+        val (item1, item3) = FocusRequester.createRefs()
+        val siblings = @Composable {
+            FocusableBox(isFocused[0], item1)
+            FocusableBox(isFocused[1])
+            FocusableBox(isFocused[2], item3)
+        }
+        val initialFocusedItem = when (focusDirection) {
+            Up, Left -> item3
+            Down, Right -> item1
+            else -> error(invalid)
+        }
+        rule.setContent {
+            focusManager = LocalFocusManager.current
+            FocusableBox {
+                FocusableBox {
+                    FocusableBox {
+                        FocusableBox {
+                            FocusableBox {
+                                FocusableBox {
+                                    when (focusDirection) {
+                                        Up, Down -> Column { siblings() }
+                                        Left, Right -> Row { siblings() }
+                                        else -> error(invalid)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        rule.runOnIdle { initialFocusedItem.requestFocus() }
+
+        // Act.
+        rule.runOnIdle { focusManager.moveFocus(focusDirection) }
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Left -> assertThat(isFocused.values).containsExactly(true, false, false)
+                Down, Right -> assertThat(isFocused.values).containsExactly(false, false, true)
+                else -> error(invalid)
+            }
+        }
+    }
 }
 
 @Composable
 private fun FocusableBox(
     isFocused: MutableState<Boolean> = mutableStateOf(false),
     focusRequester: FocusRequester? = null,
+    deactivated: Boolean = false,
     content: @Composable () -> Unit = {}
 ) {
-    FocusableBox(isFocused, 0, 0, 10, 10, focusRequester, content)
+    FocusableBox(isFocused, 0, 0, 10, 10, focusRequester, deactivated, content)
 }
 
 private val MutableList<MutableState<Boolean>>.values get() = this.map { it.value }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalOutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalOutTest.kt
index 5f2a84d..4f33fbd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalOutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalOutTest.kt
@@ -58,7 +58,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Out)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
 
         // Assert.
         rule.runOnIdle {
@@ -86,7 +86,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Out)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
 
         // Assert.
         rule.runOnIdle {
@@ -97,6 +97,35 @@
     }
 
     /**
+     *      _____________________
+     *     |  parent            |
+     *     |   _______________  |
+     *     |  | focusedItem  |  |
+     *     |  |______________|  |
+     *     |____________________|
+     */
+    @Test
+    fun focusOut_doesNotfocusOnDeactivatedParent() {
+        // Arrange.
+        val parent = mutableStateOf(false)
+        rule.setContentForTest {
+            FocusableBox(parent, 0, 0, 30, 30, deactivated = true) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isFalse()
+            assertThat(focusedItem.value).isTrue()
+            assertThat(parent.value).isFalse()
+        }
+    }
+
+    /**
      *      __________________________
      *     |  grandparent            |
      *     |   ____________________  |
@@ -120,7 +149,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Out)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
 
         // Assert.
         rule.runOnIdle {
@@ -132,6 +161,76 @@
     }
 
     /**
+     *      __________________________
+     *     |  grandparent            |
+     *     |   ____________________  |
+     *     |  |  parent           |  |
+     *     |  |   ______________  |  |
+     *     |  |  | focusedItem |  |  |
+     *     |  |  |_____________|  |  |
+     *     |  |___________________|  |
+     *     |_________________________|
+     */
+    @Test
+    fun focusOut_skipsImmediateParentIfItIsDeactivated() {
+        // Arrange.
+        val (parent, grandparent) = List(2) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(grandparent, 0, 0, 50, 50) {
+                FocusableBox(parent, 10, 10, 30, 30, deactivated = true) {
+                    FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                }
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(grandparent.value).isTrue()
+        }
+    }
+
+    /**
+     *      __________________________
+     *     |  grandparent            |
+     *     |   ____________________  |
+     *     |  |  parent           |  |
+     *     |  |   ______________  |  |
+     *     |  |  | focusedItem |  |  |
+     *     |  |  |_____________|  |  |
+     *     |  |___________________|  |
+     *     |_________________________|
+     */
+    @Test
+    fun focusOut_doesNotChangeIfAllParentsAreDeactivated() {
+        // Arrange.
+        val (parent, grandparent) = List(2) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(grandparent, 0, 0, 50, 50, deactivated = true) {
+                FocusableBox(parent, 10, 10, 30, 30, deactivated = true) {
+                    FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                }
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Out) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isFalse()
+            assertThat(focusedItem.value).isTrue()
+            assertThat(parent.value).isFalse()
+            assertThat(grandparent.value).isFalse()
+        }
+    }
+
+    /**
      *      _____________________
      *     |  parent            |
      *     |   _______________  |   ____________
@@ -151,7 +250,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Right)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Right) }
 
         // Assert.
         rule.runOnIdle {
@@ -163,6 +262,39 @@
     }
 
     /**
+     *      _____________________
+     *     |  parent            |
+     *     |   _______________  |   ___________________   ____________
+     *     |  | focusedItem  |  |  | deactivatedItem  |  | nextItem  |
+     *     |  |______________|  |  |__________________|  |___________|
+     *     |____________________|
+     */
+    @Test
+    fun focusRight_focusesOnNonDeactivatedSiblingOfParent() {
+        // Arrange.
+        val (parent, deactivated, nextItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(parent, 0, 0, 30, 30) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+            }
+            FocusableBox(deactivated, 40, 10, 10, 10, deactivated = true)
+            FocusableBox(nextItem, 40, 10, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Right) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(deactivated.value).isFalse()
+            assertThat(nextItem.value).isTrue()
+        }
+    }
+
+    /**
      *    ___________________________
      *   |  grandparent             |
      *   |   _____________________  |
@@ -187,7 +319,7 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Right)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Right) }
 
         // Assert.
         rule.runOnIdle {
@@ -230,7 +362,53 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Left)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Left) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(item1.value).isTrue()
+            assertThat(item2.value).isFalse()
+            assertThat(item3.value).isFalse()
+            assertThat(item4.value).isFalse()
+            assertThat(item5.value).isFalse()
+        }
+    }
+
+    /**
+     *                       _____________________
+     *                      |  parent            |
+     *    _______________   |   _______________  |
+     *   |    item1     |   |  |  focusedItem |  |
+     *   |______________|   |  |______________|  |
+     *    _______________   |   _______________  |
+     *   |    item2     |   |  |    item4     |  |
+     *   |______________|   |  |______________|  |
+     *    _______________   |   _______________  |
+     *   |    item3     |   |  |    item5     |  |
+     *   |______________|   |  |______________|  |
+     *                      |____________________|
+     */
+    @Test
+    fun focusLeft_fromItemOnLeftEdge_movesFocusOutsideDeactivatedParent() {
+        // Arrange.
+        val parent = mutableStateOf(false)
+        val (item1, item2, item3, item4, item5) = List(5) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 0, 10, 10, 10)
+            FocusableBox(item2, 0, 30, 10, 10)
+            FocusableBox(item3, 0, 50, 10, 10)
+            FocusableBox(parent, 20, 0, 30, 70, deactivated = true) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                FocusableBox(item4, 10, 30, 10, 10)
+                FocusableBox(item5, 10, 50, 10, 10)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Left) }
 
         // Assert.
         rule.runOnIdle {
@@ -276,7 +454,53 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Right)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Right) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(item1.value).isFalse()
+            assertThat(item2.value).isFalse()
+            assertThat(item3.value).isTrue()
+            assertThat(item4.value).isFalse()
+            assertThat(item5.value).isFalse()
+        }
+    }
+
+    /**
+     *      _____________________
+     *     |  parent            |
+     *     |   _______________  |    _______________
+     *     |  | focusedItem  |  |   |    item3     |
+     *     |  |______________|  |   |______________|
+     *     |   _______________  |    _______________
+     *     |  |    item1     |  |   |    item4     |
+     *     |  |______________|  |   |______________|
+     *     |   _______________  |    _______________
+     *     |  |    item2     |  |   |    item5     |
+     *     |  |______________|  |   |______________|
+     *     |____________________|
+     */
+    @Test
+    fun focusRight_fromItemOnRightEdge_movesFocusOutsideDeactivatedParent() {
+        // Arrange.
+        val parent = mutableStateOf(false)
+        val (item1, item2, item3, item4, item5) = List(5) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(parent, 0, 0, 30, 70, deactivated = true) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                FocusableBox(item1, 10, 30, 10, 10)
+                FocusableBox(item2, 10, 50, 10, 10)
+            }
+            FocusableBox(item3, 40, 10, 10, 10)
+            FocusableBox(item4, 40, 30, 10, 10)
+            FocusableBox(item5, 40, 50, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Right) }
 
         // Assert.
         rule.runOnIdle {
@@ -319,7 +543,50 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Up)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Up) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(item1.value).isTrue()
+            assertThat(item2.value).isFalse()
+            assertThat(item3.value).isFalse()
+            assertThat(item4.value).isFalse()
+            assertThat(item5.value).isFalse()
+        }
+    }
+
+    /**
+     *       _______________   _______________   _______________
+     *      |    item1     |  |    item2     |  |    item3     |
+     *      |______________|  |______________|  |______________|
+     *    _________________________________________________________
+     *   |   parent                                               |
+     *   |   _______________   _______________   _______________  |
+     *   |  | focusedItem  |  |    item4     |  |    item5     |  |
+     *   |  |______________|  |______________|  |______________|  |
+     *   |________________________________________________________|
+     */
+    @Test
+    fun focusUp_fromTopmostItem_movesFocusOutsideDeactivatedParent() {
+        // Arrange.
+        val parent = mutableStateOf(false)
+        val (item1, item2, item3, item4, item5) = List(5) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(item1, 10, 0, 10, 10)
+            FocusableBox(item2, 30, 0, 10, 10)
+            FocusableBox(item3, 50, 0, 10, 10)
+            FocusableBox(parent, 0, 20, 70, 30, deactivated = true) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                FocusableBox(item4, 30, 10, 10, 10)
+                FocusableBox(item5, 50, 10, 10, 10)
+            }
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Up) }
 
         // Assert.
         rule.runOnIdle {
@@ -362,7 +629,50 @@
         }
 
         // Act.
-        val movedFocusSuccessfully = focusManager.moveFocus(Down)
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Down) }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(movedFocusSuccessfully).isTrue()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(parent.value).isFalse()
+            assertThat(item1.value).isFalse()
+            assertThat(item2.value).isFalse()
+            assertThat(item3.value).isTrue()
+            assertThat(item4.value).isFalse()
+            assertThat(item5.value).isFalse()
+        }
+    }
+
+    /**
+     *    _________________________________________________________
+     *   |   parent                                               |
+     *   |   _______________   _______________   _______________  |
+     *   |  | focusedItem  |  |    item1     |  |    item2     |  |
+     *   |  |______________|  |______________|  |______________|  |
+     *   |________________________________________________________|
+     *       _______________   _______________   _______________
+     *      |    item3     |  |    item4     |  |    item5     |
+     *      |______________|  |______________|  |______________|
+     */
+    @Test
+    fun focusDown_fromBottommostItem_movesFocusOutsideDeactivatedParent() {
+        // Arrange.
+        val parent = mutableStateOf(false)
+        val (item1, item2, item3, item4, item5) = List(5) { mutableStateOf(false) }
+        rule.setContentForTest {
+            FocusableBox(parent, 0, 0, 70, 30, deactivated = true) {
+                FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+                FocusableBox(item1, 30, 10, 10, 10)
+                FocusableBox(item2, 50, 10, 10, 10)
+            }
+            FocusableBox(item3, 10, 40, 10, 10)
+            FocusableBox(item4, 30, 40, 10, 10)
+            FocusableBox(item5, 50, 40, 10, 10)
+        }
+
+        // Act.
+        val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Down) }
 
         // Assert.
         rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
index 2c956f2..13798785 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
@@ -44,8 +44,8 @@
     @get:Rule
     val rule = createComposeRule()
 
-    lateinit var inputModeManager: InputModeManager
-    lateinit var view: View
+    private lateinit var inputModeManager: InputModeManager
+    private lateinit var view: View
 
     init {
         InstrumentationRegistry.getInstrumentation().setInTouchMode(param.inputMode == Touch)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 284ce94..e474c89 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -94,7 +94,6 @@
         AndroidPointerInputTestActivity::class.java
     )
 
-    private lateinit var androidComposeView: AndroidComposeView
     private lateinit var container: OpenComposeView
 
     @Before
@@ -238,7 +237,7 @@
     @Test
     fun dispatchTouchEvent_notMeasuredLayoutsAreMeasuredFirst() {
         val size = mutableStateOf(10)
-        var latch = CountDownLatch(1)
+        val latch = CountDownLatch(1)
         var consumedDownPosition: Offset? = null
         rule.runOnUiThread {
             container.setContent {
@@ -475,7 +474,7 @@
         var didLongPress = false
         var didTap = false
         var inputLatch = CountDownLatch(1)
-        var positionedLatch = CountDownLatch(1)
+        val positionedLatch = CountDownLatch(1)
 
         rule.runOnUiThread {
             container.setContent {
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 aff897cd..391bd08 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
@@ -342,6 +342,7 @@
     // executed whenever the touch mode changes.
     private val touchModeChangeListener = ViewTreeObserver.OnTouchModeChangeListener { touchMode ->
         _inputModeManager.inputMode = if (touchMode) Touch else Keyboard
+        _focusManager.fetchUpdatedFocusProperties()
     }
 
     private val textInputServiceAndroid = TextInputServiceAndroid(this)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index c972610..b9c3df2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -16,17 +16,18 @@
 
 package androidx.compose.ui.focus
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection.Companion.Next
-import androidx.compose.ui.focus.FocusDirection.Companion.Out
 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.node.ModifiedFocusNode
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
 
 interface FocusManager {
     /**
@@ -109,25 +110,21 @@
      */
     override fun clearFocus(force: Boolean) {
         // If this hierarchy had focus before clearing it, it indicates that the host view has
-        // focus. So after clearing focus within the compose hierarchy, we should reset the root
-        // focus modifier to "Active" to maintain consistency with the host view.
-        val rootWasFocused = when (focusModifier.focusState) {
-            Active, ActiveParent, Captured -> true
-            Disabled, Inactive -> false
-        }
-
-        if (focusModifier.focusNode.clearFocus(force) && rootWasFocused) {
-            focusModifier.focusState = Active
+        // focus. So after clearing focus within the compose hierarchy, we should restore focus to
+        // the root focus modifier to maintain consistency with the host view.
+        val rootInitialState = focusModifier.focusState
+        if (focusModifier.focusNode.clearFocus(force)) {
+            focusModifier.focusState = when (rootInitialState) {
+                Active, ActiveParent, Captured -> Active
+                Deactivated, DeactivatedParent -> Deactivated
+                Inactive -> Inactive
+            }
         }
     }
 
     /**
      * Moves focus in the specified direction.
      *
-     * Focus moving is still being implemented. Right now, focus will move only if the user
-     * specified a custom focus traversal order for the item that is currently focused. (Using the
-     * [Modifier.focusOrder()][focusOrder] API).
-     *
      * @return true if focus was moved successfully. false if the focused item is unchanged.
      */
     override fun moveFocus(focusDirection: FocusDirection): Boolean {
@@ -145,30 +142,60 @@
         }
 
         val destination = focusModifier.focusNode.focusSearch(focusDirection, layoutDirection)
-        if (destination == null || destination == source) {
+        if (destination == source) {
             return false
         }
 
-        // We don't want moveFocus to set focus to the root, as this would essentially clear focus.
-        if (destination.findParentFocusNode() == null) {
-            return when (focusDirection) {
-                // Skip the root and proceed to the next/previous item from the root's perspective.
-                Next, Previous -> {
-                    destination.requestFocus(propagateFocus = false)
-                    moveFocus(focusDirection)
-                }
-                // Instead of moving out to the root, we return false.
-                // When we return false the key event will not be consumed, but it will bubble
-                // up to the owner. (In the case of Android, the back key will be sent to the
-                // activity, where it can be handled appropriately).
-                @OptIn(ExperimentalComposeUiApi::class)
-                Out -> false
-                else -> error("Move focus landed at the root through an unknown path.")
+        // TODO(b/144116848): This is a hack to make Next/Previous wrap around. This must be
+        //  replaced by code that sends the move request back to the view system. The view system
+        //  will then pass focus to other views, and ultimately return back to this compose view.
+        if (destination == null) {
+            // Check if we need to wrap around (no destination and a non-root item is focused)
+            if (focusModifier.focusState.hasFocus && !focusModifier.focusState.isFocused) {
+                    // Next and Previous wraps around.
+                    return when (focusDirection) {
+                        Next, Previous -> {
+                            // Clear Focus to send focus the root node.
+                            // Wrap around by requesting focus for the root and then calling moveFocus.
+                            clearFocus(force = false)
+
+                            if (focusModifier.focusState.isFocused) {
+                                moveFocus(focusDirection)
+                            } else {
+                                false
+                            }
+                        }
+                        else -> false
+                    }
             }
+            return false
         }
 
+        checkNotNull(destination.findParentFocusNode()) { "Move focus landed at the root." }
+
         // If we found a potential next item, call requestFocus() to move focus to it.
         destination.requestFocus(propagateFocus = false)
         return true
     }
+
+    /**
+     * Runs the focus properties block for all [focusProperties] modifiers to fetch updated
+     * [FocusProperties].
+     *
+     * The [focusProperties] block is run automatically whenever the properties change, and you
+     * rarely need to invoke this function manually. However, if you have a situation where you want
+     * to change a property, and need to see the change in the current snapshot, use this API.
+     */
+    fun fetchUpdatedFocusProperties() {
+        focusModifier.focusNode.updateProperties()
+    }
+}
+
+private fun ModifiedFocusNode.updateProperties() {
+    // Update the focus node with the current focus properties.
+    with(modifier.modifierLocalReadScope) {
+        setUpdatedProperties(ModifierLocalFocusProperties.current)
+    }
+    // Update the focus properties for all children.
+    focusableChildren(excludeDeactivated = false).fastForEach { it.updateProperties() }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
index 81121a9..770a4eb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
@@ -20,6 +20,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
 import androidx.compose.ui.node.ModifiedFocusNode
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
@@ -36,16 +39,35 @@
     //  Set this value in AndroidComposeView, and other places where we create a focus modifier
     //  using this internal constructor.
     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
-) : Modifier.Element, InspectorValueInfo(inspectorInfo) {
+) : ModifierLocalConsumer,
+    ModifierLocalProvider<FocusProperties>,
+    InspectorValueInfo(inspectorInfo) {
 
     // TODO(b/188684110): Move focusState and focusedChild to ModifiedFocusNode and make this
     //  modifier stateless.
-
     var focusState: FocusStateImpl = initialFocus
 
     var focusedChild: ModifiedFocusNode? = null
 
     lateinit var focusNode: ModifiedFocusNode
+
+    lateinit var modifierLocalReadScope: ModifierLocalReadScope
+
+    // Reading the FocusProperties ModifierLocal.
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+        modifierLocalReadScope = scope
+
+        // Update the focus node with the current focus properties.
+        with(scope) {
+            focusNode.setUpdatedProperties(ModifierLocalFocusProperties.current)
+        }
+    }
+
+    override val key = ModifierLocalFocusProperties
+
+    // Writing the FocusProperties ModifierLocal so that any child focus modifiers don't read
+    // properties that were meant for this focus modifier.
+    override val value = defaultFocusProperties
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
index 08168fb..a2b8995 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
@@ -26,11 +26,15 @@
 
 // TODO(b/152051577): Measure the performance of findFocusableChildren().
 //  Consider caching the children.
-internal fun LayoutNode.findFocusableChildren(focusableChildren: MutableList<ModifiedFocusNode>) {
+internal fun LayoutNode.findFocusableChildren(
+    focusableChildren: MutableList<ModifiedFocusNode>,
+    excludeDeactivated: Boolean
+) {
     // TODO(b/152529395): Write a test for LayoutNode.focusableChildren(). We were calling the wrong
     //  function on [LayoutNodeWrapper] but no test caught this.
-    outerLayoutNodeWrapper.findNextFocusWrapper()?.let { focusableChildren.add(it) }
-        ?: children.fastForEach { it.findFocusableChildren(focusableChildren) }
+    outerLayoutNodeWrapper.findNextFocusWrapper(excludeDeactivated)
+        ?.let { focusableChildren.add(it) }
+        ?: children.fastForEach { it.findFocusableChildren(focusableChildren, excludeDeactivated) }
 }
 
 // TODO(b/144126759): For now we always return the first focusable child. We might want to
@@ -42,11 +46,12 @@
  * @param queue a mutable list used as a queue for breadth-first search.
  */
 internal fun LayoutNode.searchChildrenForFocusNode(
-    queue: MutableVector<LayoutNode> = mutableVectorOf()
+    queue: MutableVector<LayoutNode> = mutableVectorOf(),
+    excludeDeactivated: Boolean
 ): ModifiedFocusNode? {
     // Check if any child has a focus Wrapper.
     _children.forEach { layoutNode ->
-        val focusNode = layoutNode.outerLayoutNodeWrapper.findNextFocusWrapper()
+        val focusNode = layoutNode.outerLayoutNodeWrapper.findNextFocusWrapper(excludeDeactivated)
         if (focusNode != null) {
             return focusNode
         } else {
@@ -56,7 +61,7 @@
 
     // Perform a breadth-first search through the children.
     while (queue.isNotEmpty()) {
-        val focusNode = queue.removeAt(0).searchChildrenForFocusNode(queue)
+        val focusNode = queue.removeAt(0).searchChildrenForFocusNode(queue, excludeDeactivated)
         if (focusNode != null) {
             return focusNode
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
new file mode 100644
index 0000000..a0f3166
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 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 androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.ModifiedFocusNode
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * A Modifier local that stores [FocusProperties] for a sub-hierarchy.
+ *
+ * @see [focusProperties]
+ */
+internal val ModifierLocalFocusProperties = modifierLocalOf { defaultFocusProperties }
+
+internal val defaultFocusProperties: FocusProperties = FocusPropertiesImpl(canFocus = true)
+
+/**
+ * Properties that are applied to [focusTarget]s that can read the [ModifierLocalFocusProperties]
+ * Modifier Local.
+ *
+ * @see [focusProperties]
+ */
+interface FocusProperties {
+    /**
+     * When set to false, indicates that the [focusTarget] that this is applied to can no longer
+     * take focus. If the [focusTarget] is currently focused, setting this property to false will
+     * end up clearing focus.
+     */
+    var canFocus: Boolean
+}
+
+/**
+ * This modifier allows you to specify properties that are accessible to [focusTarget]s further
+ * down the modifier chain or on child layout nodes.
+ *
+ * @sample androidx.compose.ui.samples.FocusPropertiesSample
+ */
+fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier = composed(
+    debugInspectorInfo {
+        name = "focusProperties"
+        properties["scope"] = scope
+    }
+) {
+    val rememberedScope by rememberUpdatedState(scope)
+    remember { FocusPropertiesModifier(focusPropertiesScope = rememberedScope) }
+}
+
+internal class FocusPropertiesModifier(
+    val focusPropertiesScope: FocusProperties.() -> Unit
+) : ModifierLocalConsumer, ModifierLocalProvider<FocusProperties> {
+
+    private var parentFocusProperties: FocusProperties? = null
+
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+        parentFocusProperties = scope.run { ModifierLocalFocusProperties.current }
+    }
+
+    override val key = ModifierLocalFocusProperties
+
+    override val value: FocusProperties
+        get() = defaultFocusProperties.copy {
+            // Populate with the specified focus properties.
+            apply(focusPropertiesScope)
+
+            // current value for deactivated can be overridden by a parent's value.
+            parentFocusProperties?.let {
+                if (it != defaultFocusProperties) {
+                    canFocus = it.canFocus
+                }
+            }
+        }
+}
+
+internal fun ModifiedFocusNode.setUpdatedProperties(properties: FocusProperties) {
+    if (properties.canFocus) activateNode() else deactivateNode()
+}
+
+private class FocusPropertiesImpl(override var canFocus: Boolean) : FocusProperties
+
+private fun FocusProperties.copy(scope: FocusProperties.() -> Unit): FocusProperties {
+    return FocusPropertiesImpl(canFocus = canFocus).apply(scope)
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
index 83d314f..ba5cb16 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
@@ -68,7 +68,10 @@
     Captured,
 
     // The focusable component is not currently focusable. (eg. A disabled button).
-    Disabled,
+    Deactivated,
+
+    // One of the descendants of this deactivated component is Active.
+    DeactivatedParent,
 
     // The focusable component does not receive any key events. (ie it is not active, nor are any
     // of its descendants active).
@@ -77,18 +80,30 @@
     override val isFocused: Boolean
         get() = when (this) {
             Captured, Active -> true
-            ActiveParent, Disabled, Inactive -> false
+            ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
         }
 
     override val hasFocus: Boolean
         get() = when (this) {
-            Active, ActiveParent, Captured -> true
-            Disabled, Inactive -> false
+            Active, ActiveParent, Captured, DeactivatedParent -> true
+            Deactivated, Inactive -> false
         }
 
     override val isCaptured: Boolean
         get() = when (this) {
             Captured -> true
-            Active, ActiveParent, Inactive, Disabled -> false
+            Active, ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
+        }
+
+    /**
+     * Whether the focusable component is deactivated.
+     *
+     * TODO(ralu): Consider making this public when we can add methods to interfaces without
+     * breaking compatibility.
+     */
+     val isDeactivated: Boolean
+        get() = when (this) {
+            Active, ActiveParent, Captured, Inactive -> false
+            Deactivated, DeactivatedParent -> true
         }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
index e4b2feb..6399c69 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
@@ -19,7 +19,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.node.ModifiedFocusNode
 
@@ -34,7 +35,7 @@
  */
 internal fun ModifiedFocusNode.requestFocus(propagateFocus: Boolean = true) {
     when (focusState) {
-        Active, Captured, Disabled -> {
+        Active, Captured, Deactivated, DeactivatedParent -> {
             // There is no change in focus state, but we send a focus event to notify the user
             // that the focus request is completed.
             sendOnFocusEvent(focusState)
@@ -69,6 +70,37 @@
 }
 
 /**
+ * Activate this node so that it can be focused.
+ *
+ * Deactivated nodes are excluded from focus search, and reject requests to gain focus.
+ * Calling this function activates a deactivated node.
+ */
+internal fun ModifiedFocusNode.activateNode() {
+    when (focusState) {
+        ActiveParent, Active, Captured, Inactive -> { }
+        Deactivated -> focusState = Inactive
+        DeactivatedParent -> focusState = ActiveParent
+    }
+}
+
+/**
+ * Deactivate this node so that it can't be focused.
+ *
+ * Deactivated nodes are excluded from focus search.
+ */
+internal fun ModifiedFocusNode.deactivateNode() {
+    when (focusState) {
+        ActiveParent -> focusState = DeactivatedParent
+        Active, Captured -> {
+            layoutNode.owner?.focusManager?.clearFocus(force = true)
+            focusState = Deactivated
+        }
+        Inactive -> focusState = Deactivated
+        Deactivated, DeactivatedParent -> { }
+    }
+}
+
+/**
  * Deny requests to clear focus.
  *
  * This is used when a component wants to hold onto focus (eg. A phone number field with an
@@ -82,7 +114,7 @@
         true
     }
     Captured -> true
-    else -> false
+    ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
 }
 
 /**
@@ -98,7 +130,7 @@
         true
     }
     Active -> true
-    else -> false
+    ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
 }
 
 /**
@@ -116,7 +148,7 @@
         }
         /**
          * If the node is [ActiveParent], we need to clear focus from the [Active] descendant
-         * first, before clearing focus of this node.
+         * first, before clearing focus from this node.
          */
         ActiveParent -> {
             val currentFocusedChild = focusedChild
@@ -129,6 +161,20 @@
             }
         }
         /**
+         * If the node is [DeactivatedParent], we need to clear focus from the [Active] descendant
+         * first, before clearing focus from this node.
+         */
+        DeactivatedParent -> {
+            val currentFocusedChild = focusedChild
+            requireNotNull(currentFocusedChild)
+            currentFocusedChild.clearFocus(forcedClear).also { success ->
+                if (success) {
+                    focusState = Deactivated
+                    focusedChild = null
+                }
+            }
+        }
+        /**
          * If the node is [Captured], deny requests to clear focus, except for a forced clear.
          */
         Captured -> {
@@ -140,7 +186,7 @@
         /**
          * Nothing to do if the node is not focused.
          */
-        Inactive, Disabled -> true
+        Inactive, Deactivated -> true
     }
 }
 
@@ -159,13 +205,21 @@
 
     // TODO (b/144126759): Design a system to decide which child gets focus.
     //  for now we grant focus to the first child.
-    val focusedCandidate = focusableChildren().firstOrNull()
+    val focusedCandidate = focusableChildren(excludeDeactivated = false).firstOrNull()
 
     if (focusedCandidate == null || !propagateFocus) {
         // No Focused Children, or we don't want to propagate focus to children.
-        focusState = Active
+        focusState = when (focusState) {
+            Inactive, Active, ActiveParent -> Active
+            Captured -> Captured
+            Deactivated, DeactivatedParent -> error("Granting focus to a deactivated node.")
+        }
     } else {
-        focusState = ActiveParent
+        focusState = when (focusState) {
+            Inactive, Active, ActiveParent -> ActiveParent
+            Captured -> { Captured; return }
+            Deactivated, DeactivatedParent -> DeactivatedParent
+        }
         focusedChild = focusedCandidate
         focusedCandidate.grantFocus(propagateFocus)
     }
@@ -185,7 +239,7 @@
 ): Boolean {
 
     // Only this node's children can ask for focus.
-    if (!focusableChildren().contains(childNode)) {
+    if (!focusableChildren(excludeDeactivated = false).contains(childNode)) {
         error("Non child node cannot request focus.")
     }
 
@@ -215,6 +269,23 @@
                 false
             }
         }
+        DeactivatedParent -> {
+            val previouslyFocusedNode = focusedChild
+            if (previouslyFocusedNode == null) {
+                // we use DeactivatedParent and focusedchild == null to indicate an intermediate
+                // state where a parent requested focus so that it can transfer it to a child.
+                focusedChild = childNode
+                childNode.grantFocus(propagateFocus)
+                true
+            } else if (previouslyFocusedNode.clearFocus()) {
+                focusedChild = childNode
+                childNode.grantFocus(propagateFocus)
+                true
+            } else {
+                // Currently focused component does not want to give up focus.
+                false
+            }
+        }
         /**
          * If this node is not [Active], we must gain focus first before granting it
          * to the requesting child.
@@ -241,9 +312,15 @@
          */
         Captured -> false
         /**
-         * Children of a [Disabled] parent should also be [Disabled].
+         * If this node is [Deactivated], send a requestFocusForChild to its parent to attempt to
+         * change its state to [DeactivatedParent] before granting focus to the child.
          */
-        Disabled -> error("non root FocusNode needs a focusable parent")
+        Deactivated -> {
+            activateNode()
+            val childGrantedFocus = requestFocusForChild(childNode, propagateFocus)
+            deactivateNode()
+            childGrantedFocus
+        }
     }
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index 9098b8e..7d6cea5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -28,7 +28,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.node.ModifiedFocusNode
 import androidx.compose.ui.unit.LayoutDirection
@@ -148,7 +149,11 @@
             findActiveFocusNode()?.twoDimensionalFocusSearch(direction)
         }
         @OptIn(ExperimentalComposeUiApi::class)
-        Out -> findActiveFocusNode()?.findParentFocusNode()
+        Out -> {
+            findActiveFocusNode()?.findActiveParent().let {
+                if (it == this) null else it
+            }
+        }
         else -> error(invalidFocusDirection)
     }
 }
@@ -156,7 +161,14 @@
 internal fun ModifiedFocusNode.findActiveFocusNode(): ModifiedFocusNode? {
     return when (focusState) {
         Active, Captured -> this
-        ActiveParent -> focusedChild?.findActiveFocusNode()
-        Inactive, Disabled -> null
+        ActiveParent, DeactivatedParent -> focusedChild?.findActiveFocusNode()
+        Inactive, Deactivated -> null
     }
 }
+
+internal fun ModifiedFocusNode.findActiveParent(): ModifiedFocusNode? = findParentFocusNode()?.let {
+        when (focusState) {
+            Active, Captured, Deactivated, DeactivatedParent, Inactive -> it.findActiveParent()
+            ActiveParent -> this
+        }
+    }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
index d26db77..e495a97 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
@@ -21,70 +21,138 @@
 import androidx.compose.ui.focus.FocusStateImpl.Captured
 import androidx.compose.ui.focus.FocusDirection.Companion.Next
 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.node.ModifiedFocusNode
 import androidx.compose.ui.util.fastForEach
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
 
 private const val InvalidFocusDirection = "This function should only be used for 1-D focus search"
 private const val NoActiveChild = "ActiveParent must have a focusedChild"
-private const val NotYetAvailable = "Implement this after adding API to disable a node"
 
 internal fun ModifiedFocusNode.oneDimensionalFocusSearch(
     direction: FocusDirection
-): ModifiedFocusNode = when (direction) {
-    Next -> forwardFocusSearch() ?: this
+): ModifiedFocusNode? = when (direction) {
+    Next -> forwardFocusSearch()
     Previous -> backwardFocusSearch()
     else -> error(InvalidFocusDirection)
 }
 
-private fun ModifiedFocusNode.forwardFocusSearch(): ModifiedFocusNode? = when (focusState) {
-    ActiveParent -> {
-        val focusedChild = focusedChild ?: error(NoActiveChild)
-        focusedChild.forwardFocusSearch()?.let { return it }
+private fun ModifiedFocusNode.forwardFocusSearch(): ModifiedFocusNode? {
+    when (focusState) {
+        ActiveParent, DeactivatedParent -> {
+            val focusedChild = focusedChild ?: error(NoActiveChild)
+            focusedChild.forwardFocusSearch()?.let { return it }
 
-        var currentItemIsAfterFocusedItem = false
-        // TODO(b/192681045): Instead of fetching the children and then iterating on them, add a
-        //  forEachFocusableChild function that does not allocate a list.
-        focusableChildren().fastForEach {
-            if (currentItemIsAfterFocusedItem) {
-                return it
+            // TODO(b/192681045): Instead of fetching the children and then iterating on them, add a
+            //  forEachFocusableChild() function that does not allocate a list.
+            focusableChildren(excludeDeactivated = false).forEachItemAfter(focusedChild) { child ->
+                child.forwardFocusSearch()?.let { return it }
             }
-            if (it == focusedChild) {
-                currentItemIsAfterFocusedItem = true
-            }
+            return null
         }
-        null // Couldn't find a focusable child after the current focused child.
+        Active, Captured, Deactivated -> {
+            focusableChildren(excludeDeactivated = false).fastForEach { focusableChild ->
+                focusableChild.forwardFocusSearch()?.let { return it }
+            }
+            return null
+        }
+        Inactive -> return this
     }
-    Active, Captured -> focusableChildren().firstOrNull()
-    Inactive -> this
-    Disabled -> TODO(NotYetAvailable)
 }
 
-private fun ModifiedFocusNode.backwardFocusSearch(): ModifiedFocusNode = when (focusState) {
-    ActiveParent -> {
-        val focusedChild = focusedChild ?: error(NoActiveChild)
-        when (focusedChild.focusState) {
-            ActiveParent -> focusedChild.backwardFocusSearch()
-            Active, Captured -> {
-                var previousFocusedItem: ModifiedFocusNode? = null
-                // TODO(b/192681045): Instead of fetching the children and then iterating on them, add a
-                //  forEachFocusableChild() function that does not allocate a list.
-                focusableChildren().fastForEach {
-                    if (it == focusedChild) {
-                        return previousFocusedItem?.backwardFocusSearch() ?: this
+private fun ModifiedFocusNode.backwardFocusSearch(): ModifiedFocusNode? {
+    when (focusState) {
+        ActiveParent -> {
+            val focusedChild = focusedChild ?: error(NoActiveChild)
+            when (focusedChild.focusState) {
+                ActiveParent -> return focusedChild.backwardFocusSearch() ?: focusedChild
+                DeactivatedParent -> {
+                    focusedChild.backwardFocusSearch()?.let { return it }
+                    focusableChildren(excludeDeactivated = false).forEachItemBefore(focusedChild) {
+                        it.backwardFocusSearch()?.let { return it }
                     }
-                    previousFocusedItem = it
+                    // backward search returns the parent unless it is the root
+                    // (We don't want to move focus to the root).
+                    return if (isRoot()) null else this
                 }
-                error(NoActiveChild)
+                Active, Captured -> {
+                    focusableChildren(excludeDeactivated = false).forEachItemBefore(focusedChild) {
+                        it.backwardFocusSearch()?.let { return it }
+                    }
+                    // backward search returns the parent unless it is the root
+                    // (We don't want to move focus to the root).
+                    return if (isRoot()) null else this
+                }
+                Deactivated, Inactive -> error(NoActiveChild)
             }
-            else -> error(NoActiveChild)
+        }
+        DeactivatedParent -> {
+            val focusedChild = focusedChild ?: error(NoActiveChild)
+            when (focusedChild.focusState) {
+                ActiveParent -> return focusedChild.backwardFocusSearch() ?: focusedChild
+                DeactivatedParent -> {
+                    focusedChild.backwardFocusSearch()?.let { return it }
+                    focusableChildren(excludeDeactivated = false).forEachItemBefore(focusedChild) {
+                        it.backwardFocusSearch()?.let { return it }
+                    }
+                    return null
+                }
+                Active, Captured -> {
+                    focusableChildren(excludeDeactivated = false).forEachItemBefore(focusedChild) {
+                        it.backwardFocusSearch()?.let { return it }
+                    }
+                    return null
+                }
+                Deactivated, Inactive -> error(NoActiveChild)
+            }
+        }
+        // BackwardFocusSearch Searches among siblings of the ActiveParent for a child that is
+        // focused. So this function should never be called when this node is focused. If we
+        // reached here, it indicates that this is an initial focus state, so we run the same logic
+        // as if this node was Inactive. If we can't find an item for initial focus, we return this
+        // root node as the result.
+        Active, Captured, Inactive ->
+            return focusableChildren(excludeDeactivated = true)
+                .lastOrNull()?.backwardFocusSearch() ?: this
+        // BackwardFocusSearch Searches among siblings of the ActiveParent for a child that is
+        // focused. The search excludes deactivated items, so this function should never be called
+        // on a node with a Deactivated state. If we reached here, it indicates that this is an
+        // initial focus state, so we run the same logic as if this node was Inactive. If we can't
+        // find an item for initial focus, we return null.
+        Deactivated ->
+            return focusableChildren(excludeDeactivated = true).lastOrNull()?.backwardFocusSearch()
+    }
+}
+
+private fun ModifiedFocusNode.isRoot() = findParentFocusNode() == null
+
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.forEachItemAfter(item: T, action: (T) -> Unit) {
+    contract { callsInPlace(action) }
+    var itemFound = false
+    for (index in indices) {
+        if (itemFound) {
+            action(get(index))
+        }
+        if (get(index) == item) {
+            itemFound = true
         }
     }
-    // The BackwardFocusSearch Searches among siblings of the ActiveParent for a child that is
-    // focused. So this function should never be called when this node is focused. If we reached
-    // here, it indicates an initial focus state, so we run the same logic as if this node was
-    // Inactive.
-    Active, Captured, Inactive -> focusableChildren().lastOrNull()?.backwardFocusSearch() ?: this
-    Disabled -> TODO(NotYetAvailable)
+}
+
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.forEachItemBefore(item: T, action: (T) -> Unit) {
+    contract { callsInPlace(action) }
+    var itemFound = false
+    for (index in indices.reversed()) {
+        if (itemFound) {
+            action(get(index))
+        }
+        if (get(index) == item) {
+            itemFound = true
+        }
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index cb9c350..d95db18 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -23,7 +23,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.node.ModifiedFocusNode
@@ -46,8 +47,8 @@
 ): ModifiedFocusNode? {
     return when (focusState) {
         Inactive -> this
-        Disabled -> null
-        ActiveParent -> {
+        Deactivated -> null
+        ActiveParent, DeactivatedParent -> {
             // If the focusedChild is an intermediate parent, we continue searching among it's
             // children, and return a focus node if we find one.
             val focusedChild = focusedChild ?: error(NoActiveChild)
@@ -58,13 +59,13 @@
             // Use the focus rect of the active node as the starting point and pick one of our
             // children as the next focused item.
             val activeRect = findActiveFocusNode()?.focusRect() ?: error(NoActiveChild)
-            focusableChildren().findBestCandidate(activeRect, direction)
+            focusableChildren(excludeDeactivated = true).findBestCandidate(activeRect, direction)
         }
         Active, Captured -> {
             // The 2-D focus search starts form the root. If we reached here, it means that there
             // was no intermediate node that was ActiveParent. This is an initial focus scenario.
             // We need to search among this node's children to find the best focus candidate.
-            val focusableChildren = focusableChildren()
+            val focusableChildren = focusableChildren(excludeDeactivated = true)
 
             // If there are aren't multiple children to choose from, return the first child.
             if (focusableChildren.size <= 1) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index a0af689..a78d9cc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -131,16 +131,18 @@
 
     override fun findPreviousFocusWrapper() = wrappedBy?.findPreviousFocusWrapper()
 
-    override fun findNextFocusWrapper() = wrapped.findNextFocusWrapper()
+    override fun findNextFocusWrapper(excludeDeactivated: Boolean): ModifiedFocusNode? {
+        return wrapped.findNextFocusWrapper(excludeDeactivated)
+    }
 
     override fun findLastFocusWrapper(): ModifiedFocusNode? {
         var lastFocusWrapper: ModifiedFocusNode? = null
 
         // Find last focus wrapper for the current layout node.
-        var next: ModifiedFocusNode? = findNextFocusWrapper()
+        var next: ModifiedFocusNode? = findNextFocusWrapper(excludeDeactivated = false)
         while (next != null) {
             lastFocusWrapper = next
-            next = next.wrapped.findNextFocusWrapper()
+            next = next.wrapped.findNextFocusWrapper(excludeDeactivated = false)
         }
         return lastFocusWrapper
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index 63746e2..6c3b85b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -20,7 +20,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
@@ -58,7 +59,7 @@
 
     override fun findPreviousFocusWrapper() = wrappedBy?.findPreviousFocusWrapper()
 
-    override fun findNextFocusWrapper(): ModifiedFocusNode? = null
+    override fun findNextFocusWrapper(excludeDeactivated: Boolean): ModifiedFocusNode? = null
 
     override fun findLastFocusWrapper(): ModifiedFocusNode? = findPreviousFocusWrapper()
 
@@ -70,16 +71,19 @@
         var allChildrenDisabled: Boolean? = null
         // TODO(b/192681045): Create a utility like fun LayoutNodeWrapper.forEachFocusableChild{...}
         //  that does not allocate, but just iterates over all the focusable children.
-        focusableChildren().fastForEach {
+        focusableChildren(excludeDeactivated = false).fastForEach {
             when (it.focusState) {
-                Active, ActiveParent, Captured -> { focusedChild = it; allChildrenDisabled = false }
-                Disabled -> if (allChildrenDisabled == null) { allChildrenDisabled = true }
+                Active, ActiveParent, Captured, DeactivatedParent -> {
+                    focusedChild = it
+                    allChildrenDisabled = false
+                }
+                Deactivated -> if (allChildrenDisabled == null) { allChildrenDisabled = true }
                 Inactive -> allChildrenDisabled = false
             }
         }
 
         super.propagateFocusEvent(
-            focusedChild?.focusState ?: if (allChildrenDisabled == true) Disabled else Inactive
+            focusedChild?.focusState ?: if (allChildrenDisabled == true) Deactivated else Inactive
         )
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 607c31b..4da75b6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -702,7 +702,7 @@
      * Note: This method only goes to the modifiers that follow the one wrapped by
      * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
      */
-    abstract fun findNextFocusWrapper(): ModifiedFocusNode?
+    abstract fun findNextFocusWrapper(excludeDeactivated: Boolean): ModifiedFocusNode?
 
     /**
      * Returns the last [focus node][ModifiedFocusNode] found following this [LayoutNodeWrapper].
@@ -878,10 +878,10 @@
 
     // TODO(b/152051577): Measure the performance of focusableChildren.
     //  Consider caching the children.
-    fun focusableChildren(): List<ModifiedFocusNode> {
+    fun focusableChildren(excludeDeactivated: Boolean): List<ModifiedFocusNode> {
         // Check the modifier chain that this focus node is part of. If it has a focus modifier,
         // that means you have found the only focusable child for this node.
-        val focusableChild = wrapped?.findNextFocusWrapper()
+        val focusableChild = wrapped?.findNextFocusWrapper(excludeDeactivated)
         // findChildFocusNodeInWrapperChain()
         if (focusableChild != null) {
             return listOf(focusableChild)
@@ -889,7 +889,9 @@
 
         // Go through all your children and find the first focusable node from each child.
         val focusableChildren = mutableListOf<ModifiedFocusNode>()
-        layoutNode.children.fastForEach { it.findFocusableChildren(focusableChildren) }
+        layoutNode.children.fastForEach {
+            it.findFocusableChildren(focusableChildren, excludeDeactivated)
+        }
         return focusableChildren
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
index f387e3b..7fab9d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
@@ -37,7 +37,8 @@
         // For instance, if the observer is moved to the end of the list, and there is no focus
         // modifier following this observer, it's focus state will be invalid. To solve this, we
         // always reset the focus state when a focus observer is re-used.
-        val focusNode = wrapped.findNextFocusWrapper() ?: layoutNode.searchChildrenForFocusNode()
+        val focusNode = wrapped.findNextFocusWrapper(excludeDeactivated = false)
+            ?: layoutNode.searchChildrenForFocusNode(excludeDeactivated = false)
         modifier.onFocusEvent(focusNode?.modifier?.focusState ?: Inactive)
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 0c602b2..77e9c95 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.node
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.focus.FocusModifier
 import androidx.compose.ui.focus.FocusOrder
 import androidx.compose.ui.focus.FocusState
@@ -23,7 +24,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 import androidx.compose.ui.geometry.Rect
@@ -56,7 +58,9 @@
     fun focusRect(): Rect = findRoot().localBoundingBoxOf(this, clipBounds = false)
 
     fun sendOnFocusEvent(focusState: FocusState) {
-        wrappedBy?.propagateFocusEvent(focusState)
+        if (isAttached) {
+            wrappedBy?.propagateFocusEvent(focusState)
+        }
     }
 
     override fun onModifierChanged() {
@@ -64,6 +68,7 @@
         sendOnFocusEvent(focusState)
     }
 
+    // TODO(b/202621526) Handle cases where a focus modifier is attached to a node that is focused.
     override fun attach() {
         super.attach()
         sendOnFocusEvent(focusState)
@@ -76,29 +81,44 @@
                 layoutNode.owner?.focusManager?.clearFocus(force = true)
             }
             // Propagate the state of the next focus node to any focus observers in the hierarchy.
-            ActiveParent -> {
-                // Find the next focus node.
-                val nextFocusNode = wrapped.findNextFocusWrapper()
-                    ?: layoutNode.searchChildrenForFocusNode()
-                if (nextFocusNode != null) {
-                    findParentFocusNode()?.modifier?.focusedChild = nextFocusNode
-                    sendOnFocusEvent(nextFocusNode.focusState)
-                } else {
-                    sendOnFocusEvent(Inactive)
+            ActiveParent, DeactivatedParent -> {
+                val nextFocusNode = wrapped.findNextFocusWrapper(excludeDeactivated = false)
+                    ?: layoutNode.searchChildrenForFocusNode(excludeDeactivated = false)
+                val parentFocusNode = findParentFocusNode()
+                if (parentFocusNode != null) {
+                    parentFocusNode.modifier.focusedChild = nextFocusNode
+                    if (nextFocusNode != null) {
+                        sendOnFocusEvent(nextFocusNode.focusState)
+                    } else {
+                        parentFocusNode.focusState = when (parentFocusNode.focusState) {
+                            ActiveParent -> Inactive
+                            DeactivatedParent -> Deactivated
+                            else -> parentFocusNode.focusState
+                        }
+                    }
                 }
             }
-            // TODO(b/155212782): Implement this after adding support for disabling focus modifiers.
-            Disabled -> {}
+            Deactivated -> {
+                val nextFocusNode = wrapped.findNextFocusWrapper(excludeDeactivated = false)
+                    ?: layoutNode.searchChildrenForFocusNode(excludeDeactivated = false)
+                sendOnFocusEvent(nextFocusNode?.focusState ?: Inactive)
+            }
             // Do nothing, as the nextFocusNode is also Inactive.
             Inactive -> {}
         }
-
         super.detach()
     }
 
     override fun findPreviousFocusWrapper() = this
 
-    override fun findNextFocusWrapper() = this
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun findNextFocusWrapper(excludeDeactivated: Boolean): ModifiedFocusNode? {
+        return if (modifier.focusState.isDeactivated && excludeDeactivated) {
+            super.findNextFocusWrapper(excludeDeactivated)
+        } else {
+            this
+        }
+    }
 
     override fun propagateFocusEvent(focusState: FocusState) {
         // Do nothing. Stop propagating the focus change (since we hit another focus node).
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
index 35595f9..7b80f95 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
@@ -35,7 +35,8 @@
 
     // Searches for the focus node associated with this focus requester node.
     internal fun findFocusNode(): ModifiedFocusNode? {
-        return findNextFocusWrapper() ?: layoutNode.searchChildrenForFocusNode()
+        return findNextFocusWrapper(excludeDeactivated = false)
+            ?: layoutNode.searchChildrenForFocusNode(excludeDeactivated = false)
     }
 
     override fun onModifierChanged() {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
index e18c76c..0ee1040 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
@@ -19,7 +19,8 @@
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
 import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Disabled
+import androidx.compose.ui.focus.FocusStateImpl.Deactivated
+import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.node.InnerPlaceable
 import androidx.compose.ui.node.LayoutNode
@@ -65,7 +66,7 @@
         assertThat(focusModifier.focusState).isEqualTo(
             when (initialFocusState) {
                 Inactive -> Active
-                Active, ActiveParent, Captured, Disabled -> initialFocusState
+                Active, ActiveParent, Captured, Deactivated, DeactivatedParent -> initialFocusState
             }
         )
     }
@@ -74,7 +75,7 @@
     fun releaseFocus_changesStateToInactive() {
         // Arrange.
         focusModifier.focusState = initialFocusState as FocusStateImpl
-        if (initialFocusState == ActiveParent) {
+        if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
             val childLayoutNode = LayoutNode()
             val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Active))
             focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
@@ -88,7 +89,7 @@
         assertThat(focusModifier.focusState).isEqualTo(
             when (initialFocusState) {
                 Active, ActiveParent, Captured, Inactive -> Inactive
-                Disabled -> initialFocusState
+                Deactivated, DeactivatedParent -> Deactivated
             }
         )
     }
@@ -97,7 +98,7 @@
     fun clearFocus_forced() {
         // Arrange.
         focusModifier.focusState = initialFocusState as FocusStateImpl
-        if (initialFocusState == ActiveParent) {
+        if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
             val childLayoutNode = LayoutNode()
             val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Active))
             focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
@@ -113,7 +114,8 @@
                 // If the initial state was focused, assert that after clearing the hierarchy,
                 // the root is set to Active.
                 Active, ActiveParent, Captured -> Active
-                Disabled, Inactive -> initialFocusState
+                Deactivated, DeactivatedParent -> Deactivated
+                Inactive -> Inactive
             }
         )
     }
@@ -122,7 +124,7 @@
     fun clearFocus_notForced() {
         // Arrange.
         focusModifier.focusState = initialFocusState as FocusStateImpl
-        if (initialFocusState == ActiveParent) {
+        if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
             val childLayoutNode = LayoutNode()
             val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Active))
             focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
@@ -138,24 +140,28 @@
                 // If the initial state was focused, assert that after clearing the hierarchy,
                 // the root is set to Active.
                 Active, ActiveParent -> Active
-                Captured, Disabled, Inactive -> initialFocusState
+                Deactivated, DeactivatedParent -> Deactivated
+                Captured -> Captured
+                Inactive -> Inactive
             }
         )
     }
 
     @Test
     fun clearFocus_childIsCaptured() {
-        // Arrange.
-        focusModifier.focusState = ActiveParent
-        val childLayoutNode = LayoutNode()
-        val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Captured))
-        focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
-        focusModifier.focusedChild = child
+        if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
+            // Arrange.
+            focusModifier.focusState = initialFocusState as FocusStateImpl
+            val childLayoutNode = LayoutNode()
+            val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Captured))
+            focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
+            focusModifier.focusedChild = child
 
-        // Act.
-        focusManager.clearFocus()
+            // Act.
+            focusManager.clearFocus()
 
-        // Assert.
-        assertThat(focusModifier.focusState).isEqualTo(ActiveParent)
+            // Assert.
+            assertThat(focusModifier.focusState).isEqualTo(initialFocusState)
+        }
     }
 }
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 88b74ae..66b6df8 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -95,8 +95,8 @@
     docs("androidx.core:core-role:1.1.0-alpha02")
     docs("androidx.core:core-animation:1.0.0-alpha02")
     docs("androidx.core:core-animation-testing:1.0.0-alpha02")
-    docs("androidx.core:core:1.7.0-rc01")
-    docs("androidx.core:core-ktx:1.7.0-rc01")
+    docs("androidx.core:core:1.7.0")
+    docs("androidx.core:core-ktx:1.7.0")
     docs("androidx.core:core-splashscreen:1.0.0-alpha02")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.1.0")
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
index aeef24d..fb4a4db 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
@@ -21,7 +21,7 @@
 import androidx.glance.LocalContext
 import androidx.glance.LocalSize
 import androidx.glance.Modifier
-import androidx.glance.action.launchActivityAction
+import androidx.glance.action.actionLaunchActivity
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.SizeMode
@@ -66,7 +66,7 @@
                 )
             }
             Text(content)
-            Button("Button", onClick = launchActivityAction<Activity>())
+            Button("Button", onClick = actionLaunchActivity<Activity>())
         }
     }
 }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 883b4ebb..e6c8408 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -31,7 +31,7 @@
 import androidx.glance.LocalContext
 import androidx.glance.LocalSize
 import androidx.glance.Modifier
-import androidx.glance.action.launchActivityAction
+import androidx.glance.action.actionLaunchActivity
 import androidx.glance.layout.Box
 import androidx.glance.layout.Button
 import androidx.glance.layout.Column
@@ -385,7 +385,7 @@
     @Test
     fun createButton() {
         TestGlanceAppWidget.uiDefinition = {
-            Button("Button", onClick = launchActivityAction<Activity>(), enabled = false)
+            Button("Button", onClick = actionLaunchActivity<Activity>(), enabled = false)
         }
 
         mHostRule.startHost()
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ActionRunnableBroadcastReceiver.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ActionRunnableBroadcastReceiver.kt
index ce5df1a..8c0e3ab 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ActionRunnableBroadcastReceiver.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ActionRunnableBroadcastReceiver.kt
@@ -22,10 +22,10 @@
 import android.content.Intent
 import android.net.Uri
 import androidx.glance.action.ActionRunnable
-import androidx.glance.action.UpdateAction
+import androidx.glance.action.UpdateContentAction
 
 /**
- * Responds to broadcasts from [UpdateAction] clicks by executing the associated action.
+ * Responds to broadcasts from [UpdateContentAction] clicks by executing the associated action.
  */
 internal class ActionRunnableBroadcastReceiver : BroadcastReceiver() {
 
@@ -39,7 +39,7 @@
                 "The custom work intent must contain a work class name string using extra: " +
                     ExtraClassName
             }
-            UpdateAction.run(context, className)
+            UpdateContentAction.run(context, className)
         }
     }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
index 3ca77ca..5209c78 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
@@ -38,7 +38,7 @@
 import androidx.glance.action.LaunchActivityAction
 import androidx.glance.action.LaunchActivityClassAction
 import androidx.glance.action.LaunchActivityComponentAction
-import androidx.glance.action.UpdateAction
+import androidx.glance.action.UpdateContentAction
 import androidx.glance.appwidget.action.LaunchActivityIntentAction
 import androidx.glance.layout.Dimension
 import androidx.glance.layout.HeightModifier
@@ -121,7 +121,7 @@
                 )
             rv.setOnClickPendingIntent(viewId, pendingIntent)
         }
-        is UpdateAction -> {
+        is UpdateContentAction -> {
             val pendingIntent =
                 ActionRunnableBroadcastReceiver.createPendingIntent(context, action.runnableClass)
             rv.setOnClickPendingIntent(viewId, pendingIntent)
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index e6c1d7d..2253063 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -36,7 +36,7 @@
 import androidx.compose.runtime.Composable
 import androidx.core.view.children
 import androidx.glance.Modifier
-import androidx.glance.action.launchActivityAction
+import androidx.glance.action.actionLaunchActivity
 import androidx.glance.appwidget.layout.AndroidRemoteViews
 import androidx.glance.appwidget.layout.CheckBox
 import androidx.glance.appwidget.layout.LazyColumn
@@ -684,7 +684,7 @@
         val rv = runAndTranslate {
             Button(
                 "Button",
-                onClick = launchActivityAction<Activity>(),
+                onClick = actionLaunchActivity<Activity>(),
                 enabled = true
             )
         }
@@ -700,7 +700,7 @@
         val rv = runAndTranslate {
             Button(
                 "Button",
-                onClick = launchActivityAction<Activity>(),
+                onClick = actionLaunchActivity<Activity>(),
                 enabled = false
             )
         }
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
index 9eae920..a8ddeb4e 100644
--- a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
@@ -22,7 +22,7 @@
 import androidx.glance.Modifier
 import androidx.glance.background
 import androidx.glance.action.clickable
-import androidx.glance.action.launchActivityAction
+import androidx.glance.action.actionLaunchActivity
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
 import androidx.glance.layout.Button
@@ -419,7 +419,7 @@
     fun canInflateLaunchAction() = fakeCoroutineScope.runBlockingTest {
         val content = runAndTranslate {
             Text(
-                modifier = Modifier.clickable(launchActivityAction(TestActivity::class.java)),
+                modifier = Modifier.clickable(actionLaunchActivity(TestActivity::class.java)),
                 text = "Hello World"
             )
         }
@@ -451,7 +451,7 @@
             )
             Button(
                 "Hello World",
-                onClick = launchActivityAction(TestActivity::class.java),
+                onClick = actionLaunchActivity(TestActivity::class.java),
                 modifier = Modifier.padding(1.dp),
                 style = style
             )
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index de58fc4..190036e 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -60,14 +60,14 @@
   }
 
   public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action launchActivityAction(android.content.ComponentName componentName);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action launchActivityAction(Class<T> activity);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! launchActivityAction();
+    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
   }
 
-  public final class UpdateActionKt {
-    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action updateContentAction(Class<T> runnable);
-    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! updateContentAction();
+  public final class UpdateContentActionKt {
+    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action actionUpdateContent(Class<T> runnable);
+    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! actionUpdateContent();
   }
 
 }
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index de58fc4..190036e 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -60,14 +60,14 @@
   }
 
   public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action launchActivityAction(android.content.ComponentName componentName);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action launchActivityAction(Class<T> activity);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! launchActivityAction();
+    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
   }
 
-  public final class UpdateActionKt {
-    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action updateContentAction(Class<T> runnable);
-    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! updateContentAction();
+  public final class UpdateContentActionKt {
+    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action actionUpdateContent(Class<T> runnable);
+    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! actionUpdateContent();
   }
 
 }
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index de58fc4..190036e 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -60,14 +60,14 @@
   }
 
   public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action launchActivityAction(android.content.ComponentName componentName);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action launchActivityAction(Class<T> activity);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! launchActivityAction();
+    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
   }
 
-  public final class UpdateActionKt {
-    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action updateContentAction(Class<T> runnable);
-    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! updateContentAction();
+  public final class UpdateContentActionKt {
+    method public static <T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action actionUpdateContent(Class<T> runnable);
+    method public static inline <reified T extends androidx.glance.action.ActionRunnable> androidx.glance.action.Action! actionUpdateContent();
   }
 
 }
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
index 71ff4f3..744d5f1 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
@@ -22,7 +22,7 @@
 
 /**
  * An Action defines the actions a user can take. Implementations specify what operation will be
- * performed in response to the action, eg. [launchActivityAction] creates an Action that launches
+ * performed in response to the action, eg. [actionLaunchActivity] creates an Action that launches
  * the specified [Activity].
  */
 public interface Action
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
index f75863d..f09201b 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
@@ -26,27 +26,28 @@
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class LaunchActivityComponentAction(val componentName: ComponentName) : LaunchActivityAction
+public class LaunchActivityComponentAction(val componentName: ComponentName) : LaunchActivityAction
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class LaunchActivityClassAction(val activityClass: Class<out Activity>) : LaunchActivityAction
+public class LaunchActivityClassAction(val activityClass: Class<out Activity>) :
+    LaunchActivityAction
 
 /**
  * Creates an [Action] that launches the [Activity] specified by the given [ComponentName].
  */
-public fun launchActivityAction(componentName: ComponentName): Action =
+public fun actionLaunchActivity(componentName: ComponentName): Action =
     LaunchActivityComponentAction(componentName)
 
 /**
  * Creates an [Action] that launches the specified [Activity] when triggered.
  */
-public fun <T : Activity> launchActivityAction(activity: Class<T>): Action =
+public fun <T : Activity> actionLaunchActivity(activity: Class<T>): Action =
     LaunchActivityClassAction(activity)
 
 @Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
 /**
  * Creates an [Action] that launches the specified [Activity] when triggered.
  */
-public inline fun <reified T : Activity> launchActivityAction(): Action =
-    launchActivityAction(T::class.java)
+public inline fun <reified T : Activity> actionLaunchActivity(): Action =
+    actionLaunchActivity(T::class.java)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateContentAction.kt
similarity index 80%
rename from glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateAction.kt
rename to glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateContentAction.kt
index 660929f..3ea29c2 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateAction.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/UpdateContentAction.kt
@@ -21,7 +21,7 @@
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class UpdateAction(val runnableClass: Class<out ActionRunnable>) : Action {
+public class UpdateContentAction(val runnableClass: Class<out ActionRunnable>) : Action {
     companion object {
 
         public suspend fun run(context: Context, className: String) {
@@ -42,20 +42,22 @@
  * implementing class must have a public zero argument constructor, this is used to instantiate
  * the class at runtime.
  */
-interface ActionRunnable {
+public interface ActionRunnable {
     suspend fun run(context: Context)
 }
 
 /**
- * Creates an [Action] that executes a custom [ActionRunnable] and then updates the component view.
+ * Creates an [Action] that executes a custom [ActionRunnable] and then updates the component
+ * content.
  */
-public fun <T : ActionRunnable> updateContentAction(runnable: Class<T>): Action =
-    UpdateAction(runnable)
+public fun <T : ActionRunnable> actionUpdateContent(runnable: Class<T>): Action =
+    UpdateContentAction(runnable)
 
 @Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
 /**
- * Creates an [Action] that executes a custom [ActionRunnable] and then updates the component view.
+ * Creates an [Action] that executes a custom [ActionRunnable] and then updates the component
+ * content.
  */
 // TODO(b/201418282): Add the UI update path
-public inline fun <reified T : ActionRunnable> updateContentAction(): Action =
-    updateContentAction(T::class.java)
+public inline fun <reified T : ActionRunnable> actionUpdateContent(): Action =
+    actionUpdateContent(T::class.java)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
index 7729138..4edbaa2 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
@@ -46,23 +46,23 @@
 
     @Test
     fun testLaunchActivity() {
-        val modifiers = Modifier.clickable(launchActivityAction(TestActivity::class.java))
+        val modifiers = Modifier.clickable(actionLaunchActivity(TestActivity::class.java))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
         assertIs<LaunchActivityClassAction>(modifier.action)
     }
 
     @Test
     fun testUpdate() {
-        val modifiers = Modifier.clickable(updateContentAction<TestRunnable>())
+        val modifiers = Modifier.clickable(actionUpdateContent<TestRunnable>())
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        assertIs<UpdateAction>(modifier.action)
+        assertIs<UpdateContentAction>(modifier.action)
     }
 
     @Test
     fun testLaunchFromComponent() = fakeCoroutineScope.runBlockingTest {
         val c = ComponentName("androidx.glance.action", "androidx.glance.action.TestActivity")
 
-        val modifiers = Modifier.clickable(launchActivityAction(c))
+        val modifiers = Modifier.clickable(actionLaunchActivity(c))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
         val action = assertIs<LaunchActivityComponentAction>(modifier.action)
         val component = assertNotNull(action.componentName)
@@ -74,7 +74,7 @@
     fun testLaunchFromComponentWithContext() = fakeCoroutineScope.runBlockingTest {
         val c = ComponentName(context, "androidx.glance.action.TestActivity")
 
-        val modifiers = Modifier.clickable(launchActivityAction(c))
+        val modifiers = Modifier.clickable(actionLaunchActivity(c))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
         val action = assertIs<LaunchActivityComponentAction>(modifier.action)
         val component = assertNotNull(action.componentName)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/layout/ButtonTest.kt b/glance/glance/src/test/kotlin/androidx/glance/layout/ButtonTest.kt
index db37f7b..f43aa10 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/layout/ButtonTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/layout/ButtonTest.kt
@@ -18,7 +18,7 @@
 import android.app.Activity
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.launchActivityAction
+import androidx.glance.action.actionLaunchActivity
 import androidx.glance.findModifier
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,7 +40,7 @@
     @Test
     fun createComposableButton() = fakeCoroutineScope.runBlockingTest {
         val root = runTestingComposition {
-            Button(text = "button", onClick = launchActivityAction<Activity>(), enabled = true)
+            Button(text = "button", onClick = actionLaunchActivity<Activity>(), enabled = true)
         }
 
         assertThat(root.children).hasSize(1)
@@ -53,7 +53,7 @@
     @Test
     fun createDisabledButton() = fakeCoroutineScope.runBlockingTest {
         val root = runTestingComposition {
-            Button(text = "button", onClick = launchActivityAction<Activity>(), enabled = false)
+            Button(text = "button", onClick = actionLaunchActivity<Activity>(), enabled = false)
         }
 
         assertThat(root.children).hasSize(1)
diff --git a/preference/preference/api/current.txt b/preference/preference/api/current.txt
index f8e20c5..4cc0b65 100644
--- a/preference/preference/api/current.txt
+++ b/preference/preference/api/current.txt
@@ -414,6 +414,16 @@
     method public int getPreferenceAdapterPosition(androidx.preference.Preference!);
   }
 
+  public abstract class PreferenceHeaderFragmentCompat extends androidx.fragment.app.Fragment implements androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+    ctor public PreferenceHeaderFragmentCompat();
+    method public final androidx.slidingpanelayout.widget.SlidingPaneLayout getSlidingPaneLayout();
+    method public androidx.fragment.app.Fragment? onCreateInitialDetailFragment();
+    method public abstract androidx.preference.PreferenceFragmentCompat onCreatePreferenceHeader();
+    method @CallSuper public boolean onPreferenceStartFragment(androidx.preference.PreferenceFragmentCompat caller, androidx.preference.Preference pref);
+    method public final void openPreference(androidx.preference.Preference header);
+    property public final androidx.slidingpanelayout.widget.SlidingPaneLayout slidingPaneLayout;
+  }
+
   public class PreferenceManager {
     method public androidx.preference.PreferenceScreen! createPreferenceScreen(android.content.Context!);
     method public <T extends androidx.preference.Preference> T? findPreference(CharSequence);
diff --git a/preference/preference/api/public_plus_experimental_current.txt b/preference/preference/api/public_plus_experimental_current.txt
index f8e20c5..4cc0b65 100644
--- a/preference/preference/api/public_plus_experimental_current.txt
+++ b/preference/preference/api/public_plus_experimental_current.txt
@@ -414,6 +414,16 @@
     method public int getPreferenceAdapterPosition(androidx.preference.Preference!);
   }
 
+  public abstract class PreferenceHeaderFragmentCompat extends androidx.fragment.app.Fragment implements androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+    ctor public PreferenceHeaderFragmentCompat();
+    method public final androidx.slidingpanelayout.widget.SlidingPaneLayout getSlidingPaneLayout();
+    method public androidx.fragment.app.Fragment? onCreateInitialDetailFragment();
+    method public abstract androidx.preference.PreferenceFragmentCompat onCreatePreferenceHeader();
+    method @CallSuper public boolean onPreferenceStartFragment(androidx.preference.PreferenceFragmentCompat caller, androidx.preference.Preference pref);
+    method public final void openPreference(androidx.preference.Preference header);
+    property public final androidx.slidingpanelayout.widget.SlidingPaneLayout slidingPaneLayout;
+  }
+
   public class PreferenceManager {
     method public androidx.preference.PreferenceScreen! createPreferenceScreen(android.content.Context!);
     method public <T extends androidx.preference.Preference> T? findPreference(CharSequence);
diff --git a/preference/preference/api/restricted_current.txt b/preference/preference/api/restricted_current.txt
index 4251683..1ca2328 100644
--- a/preference/preference/api/restricted_current.txt
+++ b/preference/preference/api/restricted_current.txt
@@ -439,6 +439,16 @@
     method public void onPreferenceVisibilityChange(androidx.preference.Preference!);
   }
 
+  public abstract class PreferenceHeaderFragmentCompat extends androidx.fragment.app.Fragment implements androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+    ctor public PreferenceHeaderFragmentCompat();
+    method public final androidx.slidingpanelayout.widget.SlidingPaneLayout getSlidingPaneLayout();
+    method public androidx.fragment.app.Fragment? onCreateInitialDetailFragment();
+    method public abstract androidx.preference.PreferenceFragmentCompat onCreatePreferenceHeader();
+    method @CallSuper public boolean onPreferenceStartFragment(androidx.preference.PreferenceFragmentCompat caller, androidx.preference.Preference pref);
+    method public final void openPreference(androidx.preference.Preference header);
+    property public final androidx.slidingpanelayout.widget.SlidingPaneLayout slidingPaneLayout;
+  }
+
   public class PreferenceManager {
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public PreferenceManager(android.content.Context!);
     method public androidx.preference.PreferenceScreen! createPreferenceScreen(android.content.Context!);
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 6e89cb6..6f18810 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -31,6 +31,7 @@
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.fragment:fragment-ktx:1.3.6")
     api("androidx.recyclerview:recyclerview:1.0.0")
+    api("androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01")
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/preference/preference/res/values/dimens.xml b/preference/preference/res/values/dimens.xml
index 403523e..52efb82 100644
--- a/preference/preference/res/values/dimens.xml
+++ b/preference/preference/res/values/dimens.xml
@@ -13,4 +13,6 @@
     <!-- The padding at the start of the dropdown menu within a DropDownPreference
     TODO: Pending UX discussion in b/110975540 - keeping old behaviour for now.-->
     <dimen name="preference_dropdown_padding_start">0dp</dimen>
+    <dimen name="preferences_header_width">384dp</dimen>
+    <dimen name="preferences_detail_width">300dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/preference/preference/res/values/ids.xml b/preference/preference/res/values/ids.xml
new file mode 100644
index 0000000..33aaec7
--- /dev/null
+++ b/preference/preference/res/values/ids.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <item name="preferences_sliding_pane_layout" type="id" />
+    <item name="preferences_header" type="id" />
+    <item name="preferences_detail" type="id" />
+</resources>
\ No newline at end of file
diff --git a/preference/preference/res/values/integers.xml b/preference/preference/res/values/integers.xml
new file mode 100644
index 0000000..172ff6f
--- /dev/null
+++ b/preference/preference/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <integer name="preferences_header_pane_weight">0</integer>
+    <integer name="preferences_detail_pane_weight">1</integer>
+</resources>
\ No newline at end of file
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceHeaderFragmentCompat.kt b/preference/preference/src/main/java/androidx/preference/PreferenceHeaderFragmentCompat.kt
new file mode 100644
index 0000000..54220cac
--- /dev/null
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceHeaderFragmentCompat.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2021 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.preference
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.annotation.CallSuper
+import androidx.core.view.doOnLayout
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.fragment.app.commit
+import androidx.slidingpanelayout.widget.SlidingPaneLayout
+
+/**
+ * [PreferenceHeaderFragmentCompat] implements a two-pane fragment for preferences. The list
+ * pane is a container of preference headers. Tapping on a preference header swaps out the fragment
+ * shown in the detail pane. Subclasses are expected to implement [onCreatePreferenceHeader] to
+ * provide your own [PreferenceFragmentCompat] in the list pane. The preference header hierarchy
+ * is defined by either providing an XML resource or build in code through
+ * [PreferenceFragmentCompat]. In both cases, users need to use a [PreferenceScreen] as the root
+ * component in the hierarchy.
+ *
+ * Usage:
+ *
+ * ```
+ * class TwoPanePreference : PreferenceHeaderFragmentCompat() {
+ *     override fun onCreatePreferenceHeader(): PreferenceFragmentCompat {
+ *         return PreferenceHeader()
+ *     }
+ * }
+ * ```
+ *
+ * [PreferenceHeaderFragmentCompat] handles the fragment transaction when users defines a
+ * fragment or intent associated with the preference header. By default, the initial state fragment
+ * for the detail pane is set to the associated fragment that first found in preference
+ * headers. You can override [onCreateInitialDetailFragment] to provide the custom empty state
+ * fragment for the detail pane.
+ */
+abstract class PreferenceHeaderFragmentCompat :
+    Fragment(),
+    PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+    private var onBackPressedCallback: OnBackPressedCallback? = null
+
+    /**
+     * Return the [SlidingPaneLayout] this fragment is currently controlling.
+     *
+     * @throws IllegalStateException if the SlidingPaneLayout has not been created by [onCreateView]
+     */
+    val slidingPaneLayout: SlidingPaneLayout
+        get() = requireView() as SlidingPaneLayout
+
+    @CallSuper
+    override fun onPreferenceStartFragment(
+        caller: PreferenceFragmentCompat,
+        pref: Preference
+    ): Boolean {
+        if (caller.id == R.id.preferences_header) {
+            // Opens the preference header.
+            openPreference(pref)
+            return true
+        }
+        if (caller.id == R.id.preferences_detail) {
+            // Opens an preference in detail pane.
+            val frag = childFragmentManager.fragmentFactory.instantiate(
+                requireContext().classLoader,
+                pref.fragment
+            )
+            frag.arguments = pref.extras
+
+            childFragmentManager.commit {
+                setReorderingAllowed(true)
+                replace(R.id.preferences_detail, frag)
+                setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+                addToBackStack(null)
+            }
+            return true
+        }
+        return false
+    }
+
+    private class InnerOnBackPressedCallback(
+        private val caller: PreferenceHeaderFragmentCompat
+    ) :
+        OnBackPressedCallback(true),
+        SlidingPaneLayout.PanelSlideListener {
+
+        init {
+            caller.slidingPaneLayout.addPanelSlideListener(this)
+        }
+
+        override fun handleOnBackPressed() {
+            caller.slidingPaneLayout.closePane()
+        }
+
+        override fun onPanelSlide(panel: View, slideOffset: Float) {}
+
+        override fun onPanelOpened(panel: View) {
+            // Intercept the system back button when the detail pane becomes visible.
+            isEnabled = true
+        }
+
+        override fun onPanelClosed(panel: View) {
+            // Disable intercepting the system back button when the user returns to the list pane.
+            isEnabled = false
+        }
+    }
+
+    @CallSuper
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        parentFragmentManager.commit {
+            setPrimaryNavigationFragment(this@PreferenceHeaderFragmentCompat)
+        }
+    }
+
+    @CallSuper
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        val slidingPaneLayout = buildContentView(inflater)
+        // Now create the header fragment
+        val existingHeaderFragment = childFragmentManager.findFragmentById(
+            R.id.preferences_header
+        )
+        if (existingHeaderFragment == null) {
+            onCreatePreferenceHeader().also { newHeaderFragment ->
+                childFragmentManager.commit {
+                    setReorderingAllowed(true)
+                    add(R.id.preferences_header, newHeaderFragment)
+                }
+            }
+        }
+        slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
+        return slidingPaneLayout
+    }
+
+    private fun buildContentView(inflater: LayoutInflater): SlidingPaneLayout {
+        val slidingPaneLayout = SlidingPaneLayout(inflater.context).apply {
+            id = R.id.preferences_sliding_pane_layout
+        }
+        // Add Preference Header Pane
+        val headerContainer = FragmentContainerView(inflater.context).apply {
+            id = R.id.preferences_header
+        }
+        val headerLayoutParams = SlidingPaneLayout.LayoutParams(
+            resources.getDimensionPixelSize(R.dimen.preferences_header_width),
+            MATCH_PARENT
+        ).apply {
+            weight = resources.getInteger(R.integer.preferences_header_pane_weight).toFloat()
+        }
+        slidingPaneLayout.addView(
+            headerContainer,
+            headerLayoutParams
+        )
+
+        // Add Preference Detail Pane
+        val detailContainer = FragmentContainerView(inflater.context).apply {
+            id = R.id.preferences_detail
+        }
+        val detailLayoutParams = SlidingPaneLayout.LayoutParams(
+            resources.getDimensionPixelSize(R.dimen.preferences_detail_width),
+            MATCH_PARENT
+        ).apply {
+            weight = resources.getInteger(R.integer.preferences_detail_pane_weight).toFloat()
+        }
+        slidingPaneLayout.addView(
+            detailContainer,
+            detailLayoutParams
+        )
+        return slidingPaneLayout
+    }
+
+    /**
+     * Called to supply the preference header for this fragment. The subclasses are expected
+     * to call [setPreferenceScreen(PreferenceScreen)] either directly or via helper methods
+     * such as [setPreferenceFromResource(int)] to set headers.
+     */
+    abstract fun onCreatePreferenceHeader(): PreferenceFragmentCompat
+
+    @CallSuper
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        onBackPressedCallback = InnerOnBackPressedCallback(this)
+        slidingPaneLayout.doOnLayout {
+            onBackPressedCallback!!.isEnabled =
+                slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
+        }
+        childFragmentManager.addOnBackStackChangedListener {
+            onBackPressedCallback!!.isEnabled = childFragmentManager.backStackEntryCount == 0
+        }
+        val onBackPressedDispatcherOwner = requireContext() as? OnBackPressedDispatcherOwner
+        onBackPressedDispatcherOwner?.let {
+            it.onBackPressedDispatcher.addCallback(
+                viewLifecycleOwner,
+                onBackPressedCallback!!
+            )
+        }
+    }
+
+    override fun onViewStateRestored(savedInstanceState: Bundle?) {
+        super.onViewStateRestored(savedInstanceState)
+        if (savedInstanceState == null) {
+            onCreateInitialDetailFragment()?.let {
+                childFragmentManager.commit {
+                    setReorderingAllowed(true)
+                    replace(R.id.preferences_detail, it)
+                }
+            }
+        }
+    }
+
+    /**
+     * Override this method to set initial detail fragment that to be shown. The default
+     * implementation returns the first preference that has a fragment defined on
+     * it.
+     *
+     * @return Fragment The first fragment that found in the list of preference headers.
+     */
+    open fun onCreateInitialDetailFragment(): Fragment? {
+        val headerFragment = childFragmentManager.findFragmentById(R.id.preferences_header)
+            as PreferenceFragmentCompat
+        if (headerFragment.preferenceScreen.preferenceCount <= 0) {
+            return null
+        }
+        for (index in 0 until headerFragment.preferenceScreen.preferenceCount) {
+            val header = headerFragment.preferenceScreen.getPreference(index)
+            if (header.fragment == null) {
+                continue
+            }
+            val fragment = header.fragment?.let {
+                childFragmentManager.fragmentFactory.instantiate(
+                    requireContext().classLoader,
+                    it
+                )
+            }
+            return fragment
+        }
+        return null
+    }
+
+    /**
+     * Swaps out the fragment that associated with preference header. If associated fragment is
+     * unspecified, open the preference with the given intent instead.
+     *
+     * @param header The preference header that selected
+     */
+    fun openPreference(header: Preference) {
+        if (header.fragment == null) {
+            openPreference(header.intent)
+            return
+        }
+        val fragment = header.fragment?.let {
+            childFragmentManager.fragmentFactory.instantiate(
+                requireContext().classLoader,
+                it
+            )
+        }
+
+        fragment?.apply {
+            arguments = header.extras
+        }
+
+        // Clear back stack
+        if (childFragmentManager.backStackEntryCount > 0) {
+            val entry = childFragmentManager.getBackStackEntryAt(0)
+            childFragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+        }
+
+        childFragmentManager.commit {
+            setReorderingAllowed(true)
+            replace(R.id.preferences_detail, fragment!!)
+            if (slidingPaneLayout.isOpen) {
+                setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+            }
+            slidingPaneLayout.openPane()
+        }
+    }
+
+    /**
+     * Open preference with the given intent
+     *
+     * @param intent The intent that associated with preference header
+     */
+    private fun openPreference(intent: Intent?) {
+        if (intent == null) return
+        // TODO: Change to use WindowManager ActivityView API
+        startActivity(intent)
+    }
+}
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/build.gradle b/samples/SupportPreferenceDemos/build.gradle
index 14670ed..3c25b24 100644
--- a/samples/SupportPreferenceDemos/build.gradle
+++ b/samples/SupportPreferenceDemos/build.gradle
@@ -1,12 +1,16 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
+    id("kotlin-android")
 }
 
 dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation("androidx.core:core-ktx:1.5.0")
     implementation(project(":appcompat:appcompat"))
     implementation(project(":preference:preference"))
+    implementation(project(":preference:preference-ktx"))
     implementation(project(":recyclerview:recyclerview"))
     implementation(project(":leanback:leanback"))
     implementation(project(":leanback:leanback-preference"))
-}
+}
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml b/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
index f25a041..1c0a55da 100644
--- a/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportPreferenceDemos/src/main/AndroidManifest.xml
@@ -67,5 +67,15 @@
             </intent-filter>
         </activity>
 
+        <activity
+            android:name="TwoPanePreferences"
+            android:label="@string/twopane_preferences"
+            android:theme="@style/PreferenceTheme"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.example.androidx.preference.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
index e6aba68..05aaf32 100644
--- a/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
+++ b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
@@ -24,6 +24,8 @@
 import android.widget.ListView;
 import android.widget.SimpleAdapter;
 
+import androidx.annotation.NonNull;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -39,7 +41,7 @@
     private static final String NAME = "name";
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
+    public void onCreate(@NonNull Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         SimpleAdapter adapter = new SimpleAdapter(this, getActivityList(),
                 android.R.layout.simple_list_item_1, new String[]{NAME},
@@ -49,12 +51,13 @@
 
     @Override
     @SuppressWarnings("unchecked")
-    protected void onListItemClick(ListView l, View v, int position, long id) {
+    protected void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) {
         Map<String, Object> map = (Map<String, Object>) l.getItemAtPosition(position);
         Intent intent = (Intent) map.get(INTENT);
         startActivity(intent);
     }
 
+    @NonNull
     protected List<Map<String, Object>> getActivityList() {
         List<Map<String, Object>> activityList = new ArrayList<>();
 
@@ -63,7 +66,6 @@
 
         PackageManager pm = getPackageManager();
         List<ResolveInfo> list = pm.queryIntentActivities(mainIntent, 0);
-
         for (int i = 0; i < list.size(); i++) {
             ResolveInfo info = list.get(i);
             String label = info.loadLabel(pm).toString();
diff --git a/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/TwoPanePreferences.kt b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/TwoPanePreferences.kt
new file mode 100644
index 0000000..724783e
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/TwoPanePreferences.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 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 com.example.androidx.preference
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceHeaderFragmentCompat
+
+class TwoPanePreferences : AppCompatActivity() {
+
+    class PreferenceHeaderFragmentCompatImpl : PreferenceHeaderFragmentCompat() {
+
+        class PreferenceHeader : PreferenceFragmentCompat() {
+            override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+                setPreferencesFromResource(R.xml.preference_headers, rootKey)
+            }
+        }
+
+        override fun onCreatePreferenceHeader(): PreferenceFragmentCompat {
+            return PreferenceHeader()
+        }
+    }
+
+    class BasicPreferences : PreferenceFragmentCompat() {
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            setPreferencesFromResource(R.xml.preferences_basic, rootKey)
+        }
+    }
+
+    class WidgetPreferences : PreferenceFragmentCompat() {
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            setPreferencesFromResource(R.xml.preferences_widget, rootKey)
+        }
+    }
+
+    class DialogPreferences : PreferenceFragmentCompat() {
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            setPreferencesFromResource(R.xml.preferences_dialog, rootKey)
+        }
+    }
+
+    class AdvancedPreferences : PreferenceFragmentCompat() {
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            setPreferencesFromResource(R.xml.preferences_advanced, rootKey)
+        }
+    }
+
+    class MultiPreferences : PreferenceFragmentCompat() {
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            setPreferencesFromResource(R.xml.preferences_advanced_next, rootKey)
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // Display preference header fragment as the main content
+        if (savedInstanceState == null) {
+            supportFragmentManager.beginTransaction().replace(
+                android.R.id.content,
+                PreferenceHeaderFragmentCompatImpl()
+            ).commit()
+        }
+    }
+}
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/values/strings.xml b/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
index e4b9708..acfe9b8 100644
--- a/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
+++ b/samples/SupportPreferenceDemos/src/main/res/values/strings.xml
@@ -22,6 +22,7 @@
     <!--Fragment titles-->
     <string name="preferences">Preferences</string>
     <string name="leanback_preferences">Leanback Preferences</string>
+    <string name="twopane_preferences">TwoPane Preferences</string>
 
     <!--This section is for basic attributes -->
     <string name="basic_preferences">Basic attributes</string>
@@ -85,4 +86,18 @@
 
     <string name="title_copyable_preference">Copyable preference</string>
     <string name="summary_copyable_preference">Long press on this preference to copy its summary</string>
+    <string name="summary_dialogs">Dialog preferences</string>
+    <string name="summary_basic_preferences">Basic preferences</string>
+    <string name="summary_widgets">widgets</string>
+    <string name="summary_advanced_attributes">Advanced attributes of preferences</string>
+
+    <string name="multi_preference_screen">Multi Preference Screens</string>
+    <string name="title_multi_preference_screen">Next Preference Screen</string>
+    <string name="summary_multi_preference_screen">Click to launch another preference screen</string>
+
+    <string name="title_next_preference_screen">Next Preference Screen</string>
+    <string name="title_information">Basic Information</string>
+    <string name="summary_information">Basic Information about the demo app</string>
+    <string name="title_details">Details</string>
+    <string name="summary_details">Detailed Information about the demo app</string>
 </resources>
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preference_headers.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preference_headers.xml
new file mode 100644
index 0000000..bd37919
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preference_headers.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <Preference
+        android:key="basic"
+        android:fragment="com.example.androidx.preference.TwoPanePreferences$BasicPreferences"
+        android:summary="@string/summary_basic_preferences"
+        android:title="@string/basic_preferences"
+        android:icon="@android:drawable/sym_def_app_icon"/>
+    <SwitchPreferenceCompat
+        android:key="switch"
+        android:title="@string/title_switch_preference"
+        android:summary="@string/summary_switch_preference"/>
+    <Preference
+        android:key="widgets"
+        android:fragment="com.example.androidx.preference.TwoPanePreferences$WidgetPreferences"
+        android:summary="@string/summary_widgets"
+        android:title="@string/widgets"/>
+    <Preference
+        android:key="dialogs"
+        android:fragment="com.example.androidx.preference.TwoPanePreferences$DialogPreferences"
+        android:summary="@string/summary_dialogs"
+        android:title="@string/dialogs" />
+    <Preference
+        android:key="advanced"
+        android:fragment="com.example.androidx.preference.TwoPanePreferences$AdvancedPreferences"
+        android:summary="@string/summary_advanced_attributes"
+        android:title="@string/advanced_attributes" />
+    <Preference
+        android:key="intent"
+        android:title="@string/title_intent_preference"
+        android:summary="@string/summary_intent_preference">
+        <intent android:action="android.intent.action.VIEW"
+            android:data="http://www.android.com"/>
+    </Preference>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced.xml
new file mode 100644
index 0000000..a16bb93
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <PreferenceCategory
+        android:key="advanced"
+        android:title="@string/advanced_attributes"
+        app:initialExpandedChildrenCount="1">
+
+        <Preference
+            android:key="expandable"
+            android:title="@string/title_expandable_preference"
+            android:summary="@string/summary_expandable_preference"/>
+
+        <Preference
+            android:title="@string/title_intent_preference"
+            android:summary="@string/summary_intent_preference">
+
+            <intent android:action="android.intent.action.VIEW"
+                android:data="http://www.android.com"/>
+
+        </Preference>
+
+        <SwitchPreferenceCompat
+            android:key="parent"
+            android:title="@string/title_parent_preference"
+            android:summary="@string/summary_parent_preference"/>
+
+        <SwitchPreferenceCompat
+            android:key="child"
+            android:dependency="parent"
+            android:title="@string/title_child_preference"
+            android:summary="@string/summary_child_preference"/>
+
+        <SwitchPreferenceCompat
+            android:key="toggle_summary"
+            android:title="@string/title_toggle_summary_preference"
+            android:summaryOn="@string/summary_on_toggle_summary_preference"
+            android:summaryOff="@string/summary_off_toggle_summary_preference"/>
+
+        <Preference
+            android:key="copyable"
+            android:title="@string/title_copyable_preference"
+            android:summary="@string/summary_copyable_preference"
+            android:selectable="false"
+            app:enableCopying="true"/>
+    </PreferenceCategory>
+    <PreferenceCategory
+        android:key="multi-screen"
+        android:title="@string/multi_preference_screen">
+        <Preference
+            android:title="@string/title_multi_preference_screen"
+            android:summary="@string/summary_multi_preference_screen"
+            android:fragment="com.example.androidx.preference.TwoPanePreferences$MultiPreferences"/>
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced_next.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced_next.xml
new file mode 100644
index 0000000..07562e8
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_advanced_next.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <PreferenceCategory android:title="@string/title_next_preference_screen">
+        <Preference
+            android:key="Information"
+            android:summary="@string/summary_information"
+            android:title="@string/title_information" />
+        <Preference
+            android:key="stylized"
+            android:summary="@string/summary_details"
+            android:title="@string/title_details" />
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences_basic.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_basic.xml
new file mode 100644
index 0000000..ba3be40
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_basic.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <PreferenceCategory android:title="@string/basic_preferences">
+        <Preference
+            android:key="preference"
+            android:summary="@string/summary_basic_preference"
+            android:title="@string/title_basic_preference" />
+        <Preference
+            android:key="stylized"
+            android:summary="@string/summary_stylish_preference"
+            android:title="@string/title_stylish_preference" />
+        <Preference
+            android:icon="@android:drawable/ic_menu_camera"
+            android:key="icon"
+            android:summary="@string/summary_icon_preference"
+            android:title="@string/title_icon_preference" />
+        <Preference
+            android:key="single_line_title"
+            android:summary="@string/summary_single_line_title_preference"
+            android:title="@string/title_single_line_title_preference"
+            app:singleLineTitle="true" />
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences_dialog.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_dialog.xml
new file mode 100644
index 0000000..c39432c
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_dialog.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <PreferenceCategory
+        android:title="@string/dialogs">
+
+        <EditTextPreference
+            android:key="edittext"
+            android:title="@string/title_edittext_preference"
+            app:useSimpleSummaryProvider="true"
+            android:dialogTitle="@string/dialog_title_edittext_preference"/>
+
+        <ListPreference
+            android:key="list"
+            android:title="@string/title_list_preference"
+            app:useSimpleSummaryProvider="true"
+            android:entries="@array/entries"
+            android:entryValues="@array/entry_values"
+            android:dialogTitle="@string/dialog_title_list_preference"/>
+
+        <MultiSelectListPreference
+            android:key="multi_select_list"
+            android:title="@string/title_multi_list_preference"
+            android:summary="@string/summary_multi_list_preference"
+            android:entries="@array/entries"
+            android:entryValues="@array/entry_values"
+            android:dialogTitle="@string/dialog_title_multi_list_preference"/>
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences_widget.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_widget.xml
new file mode 100644
index 0000000..6813927
--- /dev/null
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences_widget.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <PreferenceCategory
+        android:title="@string/widgets">
+
+        <CheckBoxPreference
+            android:key="checkbox"
+            android:title="@string/title_checkbox_preference"
+            android:summary="@string/summary_checkbox_preference"/>
+
+        <SwitchPreferenceCompat
+            android:key="switch"
+            android:title="@string/title_switch_preference"
+            android:summary="@string/summary_switch_preference"/>
+
+        <DropDownPreference
+            android:key="dropdown"
+            android:title="@string/title_dropdown_preference"
+            android:entries="@array/entries"
+            app:useSimpleSummaryProvider="true"
+            android:entryValues="@array/entry_values"/>
+
+        <SeekBarPreference
+            android:key="seekbar"
+            android:title="@string/title_seekbar_preference"
+            android:max="10"
+            android:defaultValue="5"/>
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file