Fixes additional dynamic modifier input failure cases

Bug: 286578275
Test: Adds additional suite of tests
Change-Id: I4168d6d8093f69ce88c01ab5b9516b2deedd0982
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 7d1d663..a52b748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -1338,15 +1338,20 @@
     private fun Modifier.dynamicPointerInputModifier(
         enabled: Boolean,
         key: Any? = Unit,
-        onPress: () -> Unit = { },
+        onEnter: () -> Unit = { },
         onMove: () -> Unit = { },
+        onPress: () -> Unit = { },
         onRelease: () -> Unit = { },
-    ) = if (enabled) {
+        onExit: () -> Unit = { },
+        ) = if (enabled) {
         pointerInput(key) {
             awaitPointerEventScope {
                 while (true) {
                     val event = awaitPointerEvent()
                     when (event.type) {
+                        PointerEventType.Enter -> {
+                            onEnter()
+                        }
                         PointerEventType.Press -> {
                             onPress()
                         }
@@ -1356,12 +1361,29 @@
                         PointerEventType.Release -> {
                             onRelease()
                         }
+                        PointerEventType.Exit -> {
+                            onExit()
+                        }
                     }
                 }
             }
         }
     } else this
 
+    private fun Modifier.dynamicPointerInputModifierWithDetectTapGestures(
+        enabled: Boolean,
+        key: Any? = Unit,
+        onTap: () -> Unit = { }
+    ) = if (enabled) {
+        pointerInput(key) {
+            detectTapGestures {
+                onTap()
+            }
+        }
+    } else {
+        this
+    }
+
     private fun Modifier.dynamicClickableModifier(
         enabled: Boolean,
         onClick: () -> Unit
@@ -1372,8 +1394,20 @@
         ) { onClick() }
     } else this
 
+    // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+    // The next ~20 tests test enabling/disabling dynamic input modifiers (both pointer input and
+    // clickable) using various combinations (touch vs. mouse, Unit vs. unique keys, nested UI
+    // elements vs. all modifiers on one UI element, etc.)
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicClickableModifierTouch_addsAbovePointerInputWithKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithKeyTouchEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1417,8 +1451,15 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicClickableModifierTouch_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyTouchEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1462,9 +1503,18 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
+     */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicClickableModifierMouse_addsAbovePointerInputWithKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithKeyMouseEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1516,9 +1566,18 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
+     */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicClickableModifierMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1570,8 +1629,16 @@
         }
     }
 
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicInputModifierTouch_addsAboveClickableWithKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithKeyTouchEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1610,8 +1677,16 @@
         }
     }
 
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicInputModifierTouch_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithUnitKeyTouchEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1649,9 +1724,22 @@
         }
     }
 
-    // Tests a dynamic pointer input AND a dynamic clickable{} above an existing pointer input.
+    /* Uses pointer input block for the non-dynamic pointer input and BOTH a clickable{} and
+     * pointer input block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
+     * inputs (both on same Box).
+     * Both the dynamic Pointer and clickable{} are disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     * 3. Touch down
+     * 4. Assert
+     * 5. Touch move
+     * 6. Assert
+     * 7. Touch up
+     * 8. Assert
+     */
     @Test
-    fun dynamicInputModifiersInTouchStream_addsAboveClickableWithUnitKey_triggersAllModifiers() {
+    fun dynamicInputAndClickableModifier_addsAbovePointerInputWithUnitKeyTouchEventsWithMove() {
         var activeDynamicClickable by mutableStateOf(false)
         var dynamicClickableCounter by mutableStateOf(0)
 
@@ -1683,6 +1771,10 @@
                 .dynamicClickableModifier(activeDynamicClickable) {
                     dynamicClickableCounter++
                 }
+                // Note the .background() above the static pointer input block
+                // TODO (jjw): Remove once bug fixed for when a dynamic pointer input follows
+                // directly after another pointer input (both using Unit key).
+                // Workaround: add a modifier between them OR use unique keys (that is, not Unit)
                 .background(Color.Green)
                 .pointerInput(Unit) {
                     originalPointerInputLambdaExecutionCount++
@@ -1775,13 +1867,19 @@
         }
     }
 
-    /*
-     * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
-     * using a hardware device with an Android device.
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierMouse_addsAboveClickableWithKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithKeyMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1827,13 +1925,19 @@
         }
     }
 
-    /*
-     * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
-     * using a hardware device with an Android device.
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierMouse_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithUnitKeyMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1878,15 +1982,26 @@
         }
     }
 
-    /* Tests dynamically adding a pointer input DURING an event stream (specifically, Hover).
-     * Hover is the only scenario where you can add a new pointer input modifier during the event
-     * stream AND receive events in the same active stream from that new pointer input modifier.
-     * It isn't possible in the down/up scenario because you add the new modifier during the down
-     * but you don't get another down until the next event stream.
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     *
+     * Tests dynamically adding a pointer input ABOVE an existing pointer input DURING an
+     * event stream (specifically, Hover).
+     *
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Assert
+     * 3. Mouse press
+     * 4. Assert
+     * 5. Mouse release
+     * 6. Assert
+     * 7. Mouse exit
+     * 8. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierHoverMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -1966,14 +2081,17 @@
         }
     }
 
-    /* This is the same as the test above, but
-     *   1. Using clickable{}
-     *   2. It enables the dynamic pointer input and starts the hover event stream in a more
-     * hacky way (using mouse click without hover which triggers hover enter on release).
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierIncompleteMouse_addsAboveClickableHackyEvents_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableIncompleteMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1996,16 +2114,6 @@
             )
         }
 
-        // Usually, a proper event stream from hardware for mouse input would be:
-        // - enter() (hover enter)
-        // - click()
-        // - exit()
-        // However, in this case, I'm just calling click() which triggers actions:
-        // - press
-        // - release
-        // - hover enter
-        // This starts a hover event stream (in a more hacky way) and also enables the dynamic
-        // pointer input to start recording events.
         rule.onNodeWithTag("myClickable").performMouseInput {
             click()
         }
@@ -2025,16 +2133,30 @@
         }
     }
 
-    /* Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     *
+     * Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
      * event stream (specifically, Hover).
      * Hover is the only scenario where you can add a new pointer input modifier during the event
      * stream AND receive events in the same active stream from that new pointer input modifier.
      * It isn't possible in the down/up scenario because you add the new modifier during the down
      * but you don't get another down until the next event stream.
+     *
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Assert
+     * 3. Mouse press
+     * 4. Assert
+     * 5. Mouse release
+     * 6. Assert
+     * 7. Mouse exit
+     * 8. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierHoverMouse_addsBelowPointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -2092,6 +2214,8 @@
         rule.runOnIdle {
             assertTrue(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
+            // Both the original and enabled dynamic pointer input modifiers will get the event
+            // since they are on the same Box.
             assertEquals(2, originalPointerInputEventCounter)
             assertEquals(1, dynamicPressCounter)
             assertEquals(0, dynamicReleaseCounter)
@@ -2123,14 +2247,16 @@
         }
     }
 
-    /* This is the same as the test above, but
-     *   1. Using pointer input
-     *   2. It enables the dynamic pointer input and starts the hover event stream with
-     *      performMouseInput { click() }
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierIncompleteMouse_addsBelowPointerInputUnitKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -2176,10 +2302,6 @@
         rule.runOnIdle {
             assertTrue(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            // click() as of today triggers only two MotionEvents (Press and Release) where hardware
-            // would trigger enter with Press and exit with Release. Either way, Compose recognizes
-            // this is a mouse input, so it creates an Enter before the press. (It won't create
-            // an exit until the mouse hovers outside the area OR a press happens outside the area.)
             assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
             assertEquals(0, dynamicPressCounter)
             assertEquals(0, dynamicReleaseCounter)
@@ -2201,14 +2323,21 @@
         }
     }
 
-    /* This is the same as the test above, but with nested boxes:
-     *   1. Using pointer input
-     *   2. It enables the dynamic pointer input and starts the hover event stream with
-     *      performMouseInput { click() }
+    /* The next set of tests uses two nested boxes inside a box. The two nested boxes each contain
+     * their own pointer input modifier (vs. the tests above that apply two pointer input modifiers
+     * to the same box).
+     */
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input.
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputNestedBoxIncompleteMouse_addsBelowPointerInputUnitKey_triggersBothModifiers() {
+    fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -2217,7 +2346,6 @@
         var activateDynamicPointerInput by mutableStateOf(false)
 
         rule.setContent {
-
             Box(Modifier.size(100.dp).testTag("myClickable")) {
                 Box(Modifier
                     .fillMaxSize()
@@ -2259,11 +2387,7 @@
         rule.runOnIdle {
             assertTrue(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            // click() as of today triggers only two MotionEvents (Press and Release) where hardware
-            // would trigger enter with Press and exit with Release. Either way, Compose recognizes
-            // this is a mouse input, so it creates an Enter before the press. (It won't create
-            // an exit until the mouse hovers outside the area OR a press happens outside the area.)
-            assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+            assertEquals(3, originalPointerInputEventCounter)
             assertEquals(0, dynamicPressCounter)
             assertEquals(0, dynamicReleaseCounter)
         }
@@ -2275,23 +2399,22 @@
         rule.runOnIdle {
             assertTrue(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            // Because the mouse is still within the box area (at first), Compose doesn't need to
-            // trigger Exit. However, because after the release event is done, the event is passed
-            // on to a separate box (new dynamic one), Compose triggers exit. Thus, the total is 6.
-            assertEquals(6, originalPointerInputEventCounter) // Press, Release, Exit
+            assertEquals(3, originalPointerInputEventCounter)
             assertEquals(1, dynamicPressCounter)
             assertEquals(1, dynamicReleaseCounter)
         }
     }
 
-    /* This is the same as the test above, but with nested boxes and toggles pointer input on/off:
-     *   1. Using pointer input
-     *   2. It enables the dynamic pointer input and starts the hover event stream with
-     *      performMouseInput { click() }
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input.
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputToggleNestedBoxIncompleteMouse_addsBelowPointerInputUnitKey_triggersProperly() {
+    fun dynamicInputNestedBox_togglesBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -2300,7 +2423,6 @@
         var activateDynamicPointerInput by mutableStateOf(false)
 
         rule.setContent {
-
             Box(Modifier.size(100.dp).testTag("myClickable")) {
                 Box(Modifier
                     .fillMaxSize()
@@ -2333,6 +2455,7 @@
                 )
                 Box(Modifier
                     .fillMaxSize()
+                    .background(Color.Cyan)
                     .dynamicPointerInputModifier(
                         enabled = activateDynamicPointerInput,
                         onPress = {
@@ -2358,11 +2481,7 @@
         rule.runOnIdle {
             assertTrue(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            // click() as of today triggers only two MotionEvents (Press and Release) where hardware
-            // would trigger enter with Press and exit with Release. Either way, Compose recognizes
-            // this is a mouse input, so it creates an Enter before the press. (It won't create
-            // an exit until the mouse hovers outside the area OR a press happens outside the area.)
-            assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+            assertEquals(3, originalPointerInputEventCounter)
             assertEquals(0, dynamicPressCounter)
             assertEquals(0, dynamicReleaseCounter)
         }
@@ -2374,24 +2493,22 @@
         rule.runOnIdle {
             assertFalse(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            // Because the mouse is still within the box area (at first), Compose doesn't need to
-            // trigger Exit. However, because after the release event is done, the event is passed
-            // on to a separate box (new dynamic one), Compose triggers exit. Thus, the total is 6.
-            assertEquals(6, originalPointerInputEventCounter) // Press, Release, Exit
+            assertEquals(3, originalPointerInputEventCounter)
             assertEquals(1, dynamicPressCounter)
             assertEquals(1, dynamicReleaseCounter)
         }
     }
 
-    /* This is the same as the test above, but it uses Foundation's detectTapGestures{} for the
-     * the first pointer input and lower level pointer input commands for the second.
-     *   1. Using pointer input
-     *   2. It enables the dynamic pointer input and starts the hover event stream with
-     *      performMouseInput { click() }
+    /* Uses Foundation's detectTapGestures{} for the non-dynamic pointer input. The dynamic pointer
+     * input uses the lower level pointer input commands.
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputToggleNestedBoxFoundationWithMouse_addsBelowWithUnitKey_triggersProperly() {
+    fun dynamicInputNestedBoxGesture_togglesBelowWithUnitKeyIncompleteMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -2451,12 +2568,1314 @@
         rule.runOnIdle {
             assertFalse(activateDynamicPointerInput)
             assertEquals(1, originalPointerInputLambdaExecutionCount)
-            assertEquals(2, originalPointerInputEventCounter)
+            assertEquals(1, originalPointerInputEventCounter)
             assertEquals(1, dynamicPressCounter)
             assertEquals(1, dynamicReleaseCounter)
         }
     }
 
+    /* Uses Foundation's detectTapGestures{} for both the non-dynamic pointer input and the
+     * dynamic pointer input (vs. the lower level pointer input commands).
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowWithKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        // This command is the same as
+        // rule.onNodeWithTag("myClickable").performTouchInput { click() }
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertEquals(0, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /*
+     * The next four tests are based on the test above (nested boxes using a pointer input
+     * modifier blocks with the Foundation Gesture detectTapGestures{}).
+     *
+     * The difference is the dynamic pointer input modifier is enabled to start (while in the
+     * other tests it is disabled to start).
+     *
+     * The tests below tests out variations (mouse vs. touch and Unit keys vs. unique keys).
+     */
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIT for key)
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithUnitKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = Unit,
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses Unit for key)
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = Unit,
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+    // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
+    // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+    /* These tests dynamically add a pointer input BEFORE or AFTER an existing pointer input DURING
+     * an event stream (specifically, Hover). Some tests use unique keys while others use UNIT as
+     * the key. Finally, some of the tests apply the modifiers to the same Box while others use
+     * sibling blocks (read the test name for details).
+     *
+     * Test name explains the test.
+     * All tests start with the dynamic pointer disabled and enable it on the first hover enter
+     *
+     * Event sequences:
+     * 1. Hover enter
+     * 2. Assert
+     * 3. Move
+     * 4. Assert
+     * 5. Move
+     * 6. Assert
+     * 7. Hover exit
+     * 8. Assert
+     */
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+                .background(Color.Green)
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Hover Exit event then a Hover Enter event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(2, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsAbovePointerInputWithKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .dynamicPointerInputModifier(
+                    key = "unique_key_1234",
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+                .background(Color.Green)
+                .pointerInput("unique_key_5678") {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Hover Exit event then a Hover Enter event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(2, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+                .background(Color.Green)
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Move event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputWithKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .pointerInput("unique_key_5678") {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+                .background(Color.Green)
+                .dynamicPointerInputModifier(
+                    key = "unique_key_1234",
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Move event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsBelowPointerInputKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("unique_key_1234") {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        key = "unique_key_5678",
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsAbovePointerInputUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsAbovePointerInputKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        key = "unique_key_5678",
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("unique_key_1234") {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+    }
+    // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
     @OptIn(ExperimentalTestApi::class)
     @Test
     @LargeTest
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index c63e2a9..7af8753 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -252,236 +252,6 @@
         assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
     }
 
-    // Inserts a new Node at the top of an existing branch (tests removal of duplicate Nodes too).
-    @Test
-    fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithOnePointerId_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId1, listOf(pifNew1, pif1, pif2, pif3, pif4))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pifNew1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif1).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif2).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif3).apply {
-                                            pointerIds.add(pointerId1)
-                                            children.add(
-                                                Node(pif4).apply {
-                                                    pointerIds.add(pointerId1)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    @Test
-    fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pif5 = PointerInputNodeMock()
-        val pif6 = PointerInputNodeMock()
-        val pif7 = PointerInputNodeMock()
-        val pif8 = PointerInputNodeMock()
-
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        val pointerId2 = PointerId(2)
-
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
-        hitPathTracker.addHitPath(pointerId2, listOf(pifNew1, pif5, pif6, pif7, pif8))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif2).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif3).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif4).apply {
-                                            pointerIds.add(pointerId1)
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-
-            children.add(
-                Node(pifNew1).apply {
-                    pointerIds.add(pointerId2)
-                    children.add(
-                        Node(pif5).apply {
-                            pointerIds.add(pointerId2)
-                            children.add(
-                                Node(pif6).apply {
-                                    pointerIds.add(pointerId2)
-                                    children.add(
-                                        Node(pif7).apply {
-                                            pointerIds.add(pointerId2)
-                                            children.add(
-                                                Node(pif8).apply {
-                                                    pointerIds.add(pointerId2)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    // Inserts a new Node inside an existing branch (tests removal of duplicate Nodes too).
-    @Test
-    fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithOnePointerId_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pifNew1, pif2, pif3, pif4))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pifNew1).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif2).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif3).apply {
-                                            pointerIds.add(pointerId1)
-                                            children.add(
-                                                Node(pif4).apply {
-                                                    pointerIds.add(pointerId1)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    @Test
-    fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithTwoPointerIds_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pif5 = PointerInputNodeMock()
-        val pif6 = PointerInputNodeMock()
-        val pif7 = PointerInputNodeMock()
-        val pif8 = PointerInputNodeMock()
-
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        val pointerId2 = PointerId(2)
-
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pifNew1, pif7, pif8))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif2).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif3).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif4).apply {
-                                            pointerIds.add(pointerId1)
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-
-            children.add(
-                Node(pif5).apply {
-                    pointerIds.add(pointerId2)
-                    children.add(
-                        Node(pif6).apply {
-                            pointerIds.add(pointerId2)
-                            children.add(
-                                Node(pifNew1).apply {
-                                    pointerIds.add(pointerId2)
-                                    children.add(
-                                        Node(pif7).apply {
-                                            pointerIds.add(pointerId2)
-                                            children.add(
-                                                Node(pif8).apply {
-                                                    pointerIds.add(pointerId2)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
     // Inserts a Node in the bottom of an existing branch (tests removal of duplicate Nodes too).
     @Test
     fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithOnePointerId_correctResult() {
@@ -492,8 +262,16 @@
         val pifNew1 = PointerInputNodeMock()
 
         val pointerId1 = PointerId(1)
+        // Modifier.Node(s) hit by the first pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+        // Clear any old hits from previous calls (does not really apply here since it's the first
+        // call)
+        hitPathTracker.removeDetachedPointerInputNodes()
+
+        // Modifier.Node(s) hit by the second pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
+        // Clear any old hits from previous calls
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -541,10 +319,18 @@
         val pointerId1 = PointerId(1)
         val pointerId2 = PointerId(2)
 
+        // Modifier.Node(s) hit by the first pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
         hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+        // Clear any old hits from previous calls (does not really apply here since it's the first
+        // call)
+        hitPathTracker.removeDetachedPointerInputNodes()
 
+        // Modifier.Node(s) hit by the second pointer input event
+        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
         hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
+        // Clear any old hits from previous calls
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1335,7 +1121,7 @@
     @Test
     fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() {
         val throwable = catchThrowable {
-            hitPathTracker.removeDetachedPointerInputFilters()
+            hitPathTracker.removeDetachedPointerInputNodes()
         }
 
         assertThat(throwable).isNull()
@@ -1373,7 +1159,7 @@
 
         // Act.
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         // Assert.
 
@@ -1451,7 +1237,7 @@
 
         hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue()
 
@@ -1478,7 +1264,7 @@
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1511,7 +1297,7 @@
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1570,7 +1356,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1648,7 +1434,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1727,7 +1513,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1825,7 +1611,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1897,7 +1683,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1971,7 +1757,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2070,7 +1856,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent()
 
@@ -2135,7 +1921,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2204,7 +1990,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2294,7 +2080,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2371,7 +2157,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent()
 
@@ -2444,7 +2230,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2524,7 +2310,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2602,7 +2388,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2654,7 +2440,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2721,7 +2507,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2787,7 +2573,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index f442482..d84b8d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -17,6 +17,9 @@
 package androidx.compose.ui.input.pointer
 
 import androidx.collection.LongSparseArray
+import androidx.collection.MutableLongObjectMap
+import androidx.collection.MutableObjectList
+import androidx.collection.mutableObjectListOf
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,8 +45,7 @@
     /*@VisibleForTesting*/
     internal val root: NodeParent = NodeParent()
 
-    // Only used when removing duplicate Nodes from the Node tree ([removeDuplicateNode]).
-    private val vectorForHandlingDuplicateNodes: MutableVector<NodeParent> = mutableVectorOf()
+    private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10)
 
     /**
      * Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
@@ -56,21 +58,34 @@
      * @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s
      * @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId].  Must be
      * ordered from ancestor to descendant.
+     * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes)
+     * that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node.
      */
-    fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {
+    fun addHitPath(
+        pointerId: PointerId,
+        pointerInputNodes: List<Modifier.Node>,
+        prunePointerIdsAndChangesNotInNodesList: Boolean = false
+    ) {
         var parent: NodeParent = root
+        hitPointerIdsAndNodes.clear()
         var merging = true
-        var nodeBranchPathToSkipDuringDuplicateNodeRemoval: Node? = null
 
         eachPin@ for (i in pointerInputNodes.indices) {
             val pointerInputNode = pointerInputNodes[i]
+
             if (merging) {
                 val node = parent.children.firstOrNull {
                     it.modifierNode == pointerInputNode
                 }
+
                 if (node != null) {
                     node.markIsIn()
                     node.pointerIds.add(pointerId)
+
+                    val mutableObjectList =
+                        hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+                    mutableObjectList.add(node)
                     parent = node
                     continue@eachPin
                 } else {
@@ -82,52 +97,30 @@
                 pointerIds.add(pointerId)
             }
 
-            if (nodeBranchPathToSkipDuringDuplicateNodeRemoval == null) {
-                // Null means this is the first new Node created that will need a new branch path
-                // (possibly from a pre-existing cached version of the node chain).
-                // If that is the case, we need to skip this path when looking for duplicate
-                // nodes to remove (that may have previously existed somewhere else in the tree).
-                nodeBranchPathToSkipDuringDuplicateNodeRemoval = node
-            } else {
-                // Every node after the top new node (that is, the top Node in the new path)
-                // could have potentially existed somewhere else in the cached node tree, and
-                // we need to remove it if we are adding it to this new branch.
-                removeDuplicateNode(node, nodeBranchPathToSkipDuringDuplicateNodeRemoval)
-            }
+            val mutableObjectList =
+                hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+            mutableObjectList.add(node)
 
             parent.children.add(node)
             parent = node
         }
-    }
 
-    /*
-     * Removes duplicate nodes when using a cached version of the node tree. Uses breadth-first
-     * search for simplicity (and because the tree will be very small).
-     */
-    private fun removeDuplicateNode(
-        duplicateNodeToRemove: Node,
-        headOfPathToSkip: Node
-    ) {
-        vectorForHandlingDuplicateNodes.clear()
-        vectorForHandlingDuplicateNodes.add(root)
-
-        while (vectorForHandlingDuplicateNodes.isNotEmpty()) {
-            val parent = vectorForHandlingDuplicateNodes.removeAt(0)
-
-            for (index in parent.children.indices) {
-                val child = parent.children[index]
-                if (child == headOfPathToSkip) continue
-                if (child.modifierNode == duplicateNodeToRemove.modifierNode) {
-                    // Assumes there is only one unique Node in the tree (not copies).
-                    // This also removes all children attached below the node.
-                    parent.children.remove(child)
-                    return
-                }
-                vectorForHandlingDuplicateNodes.add(child)
+        if (prunePointerIdsAndChangesNotInNodesList) {
+            hitPointerIdsAndNodes.forEach { key, value ->
+                removeInvalidPointerIdsAndChanges(key, value)
             }
         }
     }
 
+    // Removes pointers/changes that are not in the latest hit test
+    private fun removeInvalidPointerIdsAndChanges(
+        pointerId: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes)
+    }
+
     /**
      * Dispatches [internalPointerEvent] through the hierarchy.
      *
@@ -175,13 +168,13 @@
     }
 
     /**
-     * Removes [PointerInputFilter]s that have been removed from the component tree.
+     * Removes detached Pointer Input Modifier Nodes.
      */
     // TODO(shepshapard): Ideally, we can process the detaching of PointerInputFilters at the time
     //  that either their associated LayoutNode is removed from the three, or their
     //  associated PointerInputModifier is removed from a LayoutNode.
-    fun removeDetachedPointerInputFilters() {
-        root.removeDetachedPointerInputFilters()
+    fun removeDetachedPointerInputNodes() {
+        root.removeDetachedPointerInputModifierNodes()
     }
 }
 
@@ -272,19 +265,29 @@
         children.clear()
     }
 
+    open fun removeInvalidPointerIdsAndChanges(
+        pointerIdValue: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        children.forEach {
+            it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+        }
+    }
+
     /**
      * Removes all child [Node]s that are no longer attached to the compose tree.
      */
-    fun removeDetachedPointerInputFilters() {
+    fun removeDetachedPointerInputModifierNodes() {
         var index = 0
         while (index < children.size) {
             val child = children[index]
+
             if (!child.modifierNode.isAttached) {
-                children.removeAt(index)
                 child.dispatchCancel()
+                children.removeAt(index)
             } else {
                 index++
-                child.removeDetachedPointerInputFilters()
+                child.removeDetachedPointerInputModifierNodes()
             }
         }
     }
@@ -327,6 +330,22 @@
     private var isIn = true
     private var hasExited = true
 
+    override fun removeInvalidPointerIdsAndChanges(
+        pointerIdValue: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        if (this.pointerIds.contains(pointerIdValue)) {
+            if (!hitNodes.contains(this)) {
+                this.pointerIds.remove(pointerIdValue)
+                this.relevantChanges.remove(pointerIdValue)
+            }
+        }
+
+        children.forEach {
+            it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+        }
+    }
+
     override fun dispatchMainEventPass(
         changes: LongSparseArray<PointerInputChange>,
         parentCoordinates: LayoutCoordinates,
@@ -342,6 +361,7 @@
         return dispatchIfNeeded {
             val event = pointerEvent!!
             val size = coordinates!!.size
+
             // Dispatch on the tunneling pass.
             modifierNode.dispatchForKind(Nodes.PointerInput) {
                 it.onPointerEvent(event, PointerEventPass.Initial, size)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index f2b624c..61e45fa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -168,24 +168,37 @@
         if (pass == Main) {
             // Cursor within the surface area of this node's bounds
             if (pointerEvent.type == PointerEventType.Enter) {
-                cursorInBoundsOfNode = true
-                displayIconIfDescendantsDoNotHavePriority()
+                onEnter()
             } else if (pointerEvent.type == PointerEventType.Exit) {
-                cursorInBoundsOfNode = false
+                onExit()
+            }
+        }
+    }
+
+    private fun onEnter() {
+        cursorInBoundsOfNode = true
+        displayIconIfDescendantsDoNotHavePriority()
+    }
+
+    private fun onExit() {
+        if (cursorInBoundsOfNode) {
+            cursorInBoundsOfNode = false
+
+            if (isAttached) {
                 displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
             }
         }
     }
 
     override fun onCancelPointerInput() {
-        // We aren't processing the event (only listening for enter/exit), so we don't need to
-        // do anything.
+        // While pointer icon only really cares about enter/exit, there are some cases (dynamically
+        // adding Modifier Nodes) where a modifier might be cancelled but hasn't been detached or
+        // exited, so we need to cover that case.
+        onExit()
     }
 
     override fun onDetach() {
-        cursorInBoundsOfNode = false
-        displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
-
+        onExit()
         super.onDetach()
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 4230b9c..e514e78 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -98,15 +98,22 @@
                     val isTouchEvent = pointerInputChange.type == PointerType.Touch
                     root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
                     if (hitResult.isNotEmpty()) {
-                        hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
+                        hitPathTracker.addHitPath(
+                            pointerId = pointerInputChange.id,
+                            pointerInputNodes = hitResult,
+                            // Prunes PointerIds (and changes) to support dynamically
+                            // adding/removing pointer input modifier nodes.
+                            // Note: We do not do this for hover because hover relies on those
+                            // non hit PointerIds to trigger hover exit events.
+                            prunePointerIdsAndChangesNotInNodesList =
+                            pointerInputChange.changedToDownIgnoreConsumed()
+                        )
                         hitResult.clear()
                     }
                 }
             }
 
-            // Remove [PointerInputFilter]s that are no longer valid and refresh the offset information
-            // for those that are.
-            hitPathTracker.removeDetachedPointerInputFilters()
+            hitPathTracker.removeDetachedPointerInputNodes()
 
             // Dispatch to PointerInputFilters
             val dispatchedToSomething =