Merge changes from topic "hoverable" into androidx-main

* changes:
  Support hoverable indication, use hoverable in other modifiers and components
  Implement HoverInteraction and Modifier.hoverable
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/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)
         )