Add `anchoredDrag` function and make animateTo and snapTo to be extension functions on AnchoredDraggableState

Convert AnchoredDraggableState to allow for custom drag gestures, while making aminateTo and snapTo to be extension functions, proving that its implementations are possible through public APIs.

Test: Added new tests and old test pass
Bug: n/a
Change-Id: I306c8c6933e3cf1c1b2a040ba2d80ca257f5b932
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
index ea63b84..e398ae4 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
@@ -21,13 +21,14 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
-import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
-import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.AnchoredDraggableDefaults.ReconcileAnimationOnAnchorChangedCallback
 import androidx.compose.material.AnchoredDraggableState
+import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
+import androidx.compose.material.animateTo
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
index d27b292..aa3c2e2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -22,13 +22,14 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.AnchoredDraggableState
 import androidx.compose.material.AutoTestFrameClock
 import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.AnchoredDraggableState
+import androidx.compose.material.anchoredDraggable
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
-import androidx.compose.material.anchoredDraggable
+import androidx.compose.material.animateTo
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.testutils.WithTouchSlop
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
index e0f833e6..d95fcb5 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -26,15 +26,17 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
-import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.AnchoredDraggableDefaults
 import androidx.compose.material.AnchoredDraggableState
-import androidx.compose.material.rememberAnchoredDraggableState
+import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.anchoredDraggable
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
 import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
-import androidx.compose.material.anchoredDraggable
+import androidx.compose.material.animateTo
+import androidx.compose.material.rememberAnchoredDraggableState
+import androidx.compose.material.snapTo
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MonotonicFrameClock
 import androidx.compose.runtime.SideEffect
@@ -43,6 +45,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -64,7 +67,9 @@
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import org.junit.Ignore
@@ -660,8 +665,8 @@
         var anchorChangeHandlerInvoked = false
         val testAnchorChangeHandler =
             AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
-            anchorChangeHandlerInvoked = true
-        }
+                anchorChangeHandlerInvoked = true
+            }
         val state = AnchoredDraggableState(
             initialValue = A,
             positionalThreshold = defaultPositionalThreshold,
@@ -677,8 +682,8 @@
         var anchorChangeHandlerInvoked = false
         val testAnchorChangedCallback =
             AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
-            anchorChangeHandlerInvoked = true
-        }
+                anchorChangeHandlerInvoked = true
+            }
         val state = AnchoredDraggableState(
             initialValue = A,
             positionalThreshold = defaultPositionalThreshold,
@@ -694,8 +699,8 @@
         var anchorChangeHandlerInvoked = false
         val testAnchorChangedCallback =
             AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
-            anchorChangeHandlerInvoked = true
-        }
+                anchorChangeHandlerInvoked = true
+            }
         val state = AnchoredDraggableState(
             initialValue = A,
             positionalThreshold = defaultPositionalThreshold,
@@ -711,6 +716,96 @@
     }
 
     @Test
+    fun anchoredDraggable_customDrag_updatesOffset() = runBlocking {
+
+        val state = AnchoredDraggableState(
+            initialValue = A,
+            positionalThreshold = defaultPositionalThreshold,
+            velocityThreshold = defaultVelocityThreshold
+        )
+        val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+        state.updateAnchors(anchors)
+        state.anchoredDrag {
+            dragTo(150f)
+        }
+
+        assertThat(state.requireOffset()).isEqualTo(150f)
+
+        state.anchoredDrag {
+            dragTo(250f)
+        }
+        assertThat(state.requireOffset()).isEqualTo(250f)
+    }
+
+    @Test
+    fun anchoredDraggable_customDrag_updatesVelocity() = runBlocking {
+
+        val state = AnchoredDraggableState(
+            initialValue = A,
+            positionalThreshold = defaultPositionalThreshold,
+            velocityThreshold = defaultVelocityThreshold
+        )
+        val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+        state.updateAnchors(anchors)
+        state.anchoredDrag {
+            dragTo(150f, lastKnownVelocity = 454f)
+        }
+        assertThat(state.lastVelocity).isEqualTo(454f)
+    }
+
+    @Test
+    fun anchoredDraggable_customDrag_targetValueUpdate() = runBlocking {
+        val clock = HandPumpTestFrameClock()
+        val dragScope = CoroutineScope(clock)
+
+        val state = AnchoredDraggableState(
+            initialValue = A,
+            positionalThreshold = defaultPositionalThreshold,
+            velocityThreshold = defaultVelocityThreshold
+        )
+        val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+        state.updateAnchors(anchors)
+        dragScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            state.anchoredDrag(targetValue = C) {
+                while (isActive) {
+                    withFrameNanos {
+                        dragTo(200f)
+                    }
+                }
+            }
+        }
+        clock.advanceByFrame()
+        assertThat(state.targetValue).isEqualTo(C)
+        dragScope.cancel()
+    }
+
+    @Test
+    fun anchoredDraggable_customDrag_anchorsPropagation() = runBlocking {
+        val clock = HandPumpTestFrameClock()
+        val dragScope = CoroutineScope(clock)
+
+        val state = AnchoredDraggableState(
+            initialValue = A,
+            positionalThreshold = defaultPositionalThreshold,
+            velocityThreshold = defaultVelocityThreshold
+        )
+        val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+        var providedAnchors = emptyMap<AnchoredDraggableTestValue, Float>()
+
+        state.updateAnchors(anchors)
+        dragScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            state.anchoredDrag(targetValue = C) { anchors ->
+                providedAnchors = anchors
+            }
+        }
+        clock.advanceByFrame()
+        assertThat(providedAnchors).isEqualTo(anchors)
+    }
+
+    @Test
     fun anchoredDraggable_updateAnchors_ongoingOffsetMutation_shouldNotUpdate() = runBlocking {
         val clock = HandPumpTestFrameClock()
         val animationScope = CoroutineScope(clock)
@@ -719,8 +814,8 @@
         var anchorChangeHandlerInvoked = false
         val testAnchorChangedCallback =
             AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
-            anchorChangeHandlerInvoked = true
-        }
+                anchorChangeHandlerInvoked = true
+            }
         val state = AnchoredDraggableState(
             initialValue = A,
             animationSpec = tween(animationDuration),
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
index b3a2bfe..6e204b8 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
@@ -71,7 +71,7 @@
     reverseDirection: Boolean = false,
     interactionSource: MutableInteractionSource? = null
 ) = draggable(
-    state = state.swipeDraggableState,
+    state = state.draggableState,
     orientation = orientation,
     enabled = enabled,
     interactionSource = interactionSource,
@@ -81,6 +81,26 @@
 )
 
 /**
+ * Scope used for suspending anchored drag blocks. Allows to set [AnchoredDraggableState.offset] to
+ * a new value.
+ *
+ * @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
+ * access to this scope.
+ */
+internal interface AnchoredDragScope {
+    /**
+     * Assign a new value for an offset value for [AnchoredDraggableState].
+     *
+     * @param newOffset new value for [AnchoredDraggableState.offset].
+     * @param lastKnownVelocity last known velocity (if known)
+     */
+    fun dragTo(
+        newOffset: Float,
+        lastKnownVelocity: Float = 0f
+    )
+}
+
+/**
  * State of the [anchoredDraggable] modifier.
  *
  * This contains necessary information about any ongoing drag or animation and provides methods
@@ -104,16 +124,19 @@
     initialValue: T,
     internal val positionalThreshold: (totalDistance: Float) -> Float,
     internal val velocityThreshold: () -> Float,
-    internal val animationSpec: AnimationSpec<Float> = AnchoredDraggableDefaults.AnimationSpec,
+    val animationSpec: AnimationSpec<Float> = AnchoredDraggableDefaults.AnimationSpec,
     internal val confirmValueChange: (newValue: T) -> Boolean = { true }
 ) {
 
     private val dragMutex = InternalMutatorMutex()
 
-    internal val swipeDraggableState = object : DraggableState {
+    internal val draggableState = object : DraggableState {
+
         private val dragScope = object : DragScope {
             override fun dragBy(pixels: Float) {
-                [email protected](pixels)
+                with(anchoredDragScope) {
+                    dragTo(newOffsetForDelta(pixels))
+                }
             }
         }
 
@@ -121,7 +144,9 @@
             dragPriority: MutatePriority,
             block: suspend DragScope.() -> Unit
         ) {
-            [email protected](dragPriority) { dragScope.block() }
+            [email protected](dragPriority) {
+                with(dragScope) { block() }
+            }
         }
 
         override fun dispatchRawDelta(delta: Float) {
@@ -250,7 +275,7 @@
 
             val currentValueHasAnchor = anchors[currentValue] != null
             if (previousAnchorsEmpty && currentValueHasAnchor) {
-                snap(currentValue)
+                trySnapTo(currentValue)
             } else {
                 onAnchorsChanged?.onAnchorsChanged(
                     previousTargetValue = previousTarget,
@@ -267,66 +292,6 @@
     fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
 
     /**
-     * Snap to a [targetValue] without any animation.
-     * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
-     * [targetValue] without updating the offset.
-     *
-     * @throws CancellationException if the interaction interrupted by another interaction like a
-     * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
-     *
-     * @param targetValue The target value of the animation
-     */
-    suspend fun snapTo(targetValue: T) {
-        drag { snap(targetValue) }
-    }
-
-    /**
-     * Animate to a [targetValue].
-     * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
-     * [targetValue] without updating the offset.
-     *
-     * @throws CancellationException if the interaction interrupted by another interaction like a
-     * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
-     *
-     * @param targetValue The target value of the animation
-     * @param velocity The velocity the animation should start with, [lastVelocity] by default
-     */
-    suspend fun animateTo(
-        targetValue: T,
-        velocity: Float = lastVelocity,
-    ) {
-        val targetOffset = anchors[targetValue]
-        if (targetOffset != null) {
-            try {
-                drag {
-                    animationTarget = targetValue
-                    var prev = offset ?: 0f
-                    animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
-                        // Our onDrag coerces the value within the bounds, but an animation may
-                        // overshoot, for example a spring animation or an overshooting interpolator
-                        // We respect the user's intention and allow the overshoot, but still use
-                        // DraggableState's drag for its mutex.
-                        offset = value
-                        prev = value
-                        lastVelocity = velocity
-                    }
-                    lastVelocity = 0f
-                }
-            } finally {
-                animationTarget = null
-                val endOffset = requireOffset()
-                val endState = anchors
-                    .entries
-                    .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
-                    ?.key
-                this.currentValue = endState ?: currentValue
-            }
-        } else {
-            currentValue = targetValue
-        }
-    }
-
-    /**
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
@@ -344,22 +309,6 @@
         }
     }
 
-    /**
-     * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
-     *
-     * @return The delta the consumed by the [AnchoredDraggableState]
-     */
-    fun dispatchRawDelta(delta: Float): Float {
-        val currentDragPosition = offset ?: 0f
-        val potentiallyConsumed = currentDragPosition + delta
-        val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
-        val deltaToConsume = clamped - currentDragPosition
-        if (abs(deltaToConsume) >= 0) {
-            offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
-        }
-        return deltaToConsume
-    }
-
     private fun computeTarget(
         offset: Float,
         currentValue: T,
@@ -401,10 +350,97 @@
         }
     }
 
-    private suspend fun drag(
-        priority: MutatePriority = MutatePriority.Default,
-        action: suspend () -> Unit
-    ): Unit = coroutineScope { dragMutex.mutate(priority, action) }
+    private val anchoredDragScope: AnchoredDragScope = object : AnchoredDragScope {
+        override fun dragTo(newOffset: Float, lastKnownVelocity: Float) {
+            offset = newOffset
+            lastVelocity = lastKnownVelocity
+        }
+    }
+
+    /**
+     * Call this function to take control of drag logic and perform anchored drag.
+     *
+     * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+     * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+     * in order to guarantee that mutual exclusion is enforced.
+     *
+     * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+     * drag, ongoing drag will be canceled.
+     *
+     * @param dragPriority of the drag operation
+     * @param block perform anchored drag given the current anchor provided
+     */
+    suspend fun anchoredDrag(
+        dragPriority: MutatePriority = MutatePriority.Default,
+        block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+    ): Unit = doAnchoredDrag(null, dragPriority, block)
+
+    /**
+     * Call this function to take control of drag logic and perform anchored drag.
+     *
+     * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+     * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+     * in order to guarantee that mutual exclusion is enforced.
+     *
+     * This overload allows the caller to hint the target value that this [anchoredDrag] is intended
+     * to arrive to. This will set [AnchoredDraggableState.targetValue] to provided value so
+     * consumers can reflect it in their UIs.
+     *
+     * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+     * drag, ongoing drag will be canceled.
+     *
+     * @param targetValue hint the target value that this [anchoredDrag] is intended to arrive to
+     * @param dragPriority of the drag operation
+     * @param block perform anchored drag given the current anchor provided
+     */
+    suspend fun anchoredDrag(
+        targetValue: T,
+        dragPriority: MutatePriority = MutatePriority.Default,
+        block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+    ): Unit = doAnchoredDrag(targetValue, dragPriority, block)
+
+    private suspend fun doAnchoredDrag(
+        targetValue: T?,
+        dragPriority: MutatePriority,
+        block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+    ) = coroutineScope {
+        if (targetValue == null || anchors.containsKey(targetValue)) {
+            try {
+                dragMutex.mutate(dragPriority) {
+                    if (targetValue != null) animationTarget = targetValue
+                    anchoredDragScope.block(anchors)
+                }
+            } finally {
+                if (targetValue != null) animationTarget = null
+                val endState = offset?.let { endOffset ->
+                    anchors
+                        .entries
+                        .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
+                        ?.key
+                }
+                if (endState != null && confirmValueChange.invoke(endState)) {
+                    currentValue = endState
+                }
+            }
+        } else if (confirmValueChange(targetValue)) {
+            currentValue = targetValue
+        }
+    }
+
+    internal fun newOffsetForDelta(delta: Float) =
+        ((offset ?: 0f) + delta).coerceIn(minOffset, maxOffset)
+
+    /**
+     * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
+     *
+     * @return The delta the consumed by the [AnchoredDraggableState]
+     */
+    fun dispatchRawDelta(delta: Float): Float {
+        val newOffset = newOffsetForDelta(delta)
+        val oldOffset = offset ?: 0f
+        offset = newOffset
+        return newOffset - oldOffset
+    }
 
     /**
      * Attempt to snap synchronously. Snapping can happen synchronously when there is no other drag
@@ -413,15 +449,13 @@
      *
      * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
      */
-    internal fun trySnapTo(targetValue: T): Boolean = dragMutex.tryMutate { snap(targetValue) }
-
-    private fun snap(targetValue: T) {
-        val targetOffset = anchors[targetValue]
-        if (targetOffset != null) {
-            dispatchRawDelta(targetOffset - (offset ?: 0f))
-            currentValue = targetValue
-            animationTarget = null
-        } else {
+    internal fun trySnapTo(targetValue: T): Boolean = dragMutex.tryMutate {
+        with(anchoredDragScope) {
+            val targetOffset = anchors[targetValue]
+            if (targetOffset != null) {
+                dragTo(targetOffset)
+                animationTarget = null
+            }
             currentValue = targetValue
         }
     }
@@ -480,6 +514,56 @@
 }
 
 /**
+ * Snap to a [targetValue] without any animation.
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ */
+@ExperimentalMaterialApi
+internal suspend fun <T> AnchoredDraggableState<T>.snapTo(targetValue: T) {
+    anchoredDrag(targetValue = targetValue) { anchors ->
+        val targetOffset = anchors[targetValue]
+        if (targetOffset != null) dragTo(targetOffset)
+    }
+}
+
+/**
+ * Animate to a [targetValue].
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ * @param velocity The velocity the animation should start with
+ */
+@ExperimentalMaterialApi
+internal suspend fun <T> AnchoredDraggableState<T>.animateTo(
+    targetValue: T,
+    velocity: Float = this.lastVelocity,
+) {
+    anchoredDrag(targetValue = targetValue) { anchors ->
+        val targetOffset = anchors[targetValue]
+        if (targetOffset != null) {
+            var prev = offset ?: 0f
+            animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
+                // Our onDrag coerces the value within the bounds, but an animation may
+                // overshoot, for example a spring animation or an overshooting interpolator
+                // We respect the user's intention and allow the overshoot, but still use
+                // DraggableState's drag for its mutex.
+                dragTo(value, velocity)
+                prev = value
+            }
+        }
+    }
+}
+
+/**
  * Create and remember a [AnchoredDraggableState].
  *
  * @param initialValue The initial value.
@@ -562,20 +646,20 @@
         state: AnchoredDraggableState<T>,
         scope: CoroutineScope
     ) = AnchorChangedCallback<T> { previousTarget, previousAnchors, newAnchors ->
-            val previousTargetOffset = previousAnchors[previousTarget]
-            val newTargetOffset = newAnchors[previousTarget]
-            if (previousTargetOffset != newTargetOffset) {
-                if (newTargetOffset != null) {
-                    scope.launch {
-                        state.animateTo(previousTarget, state.lastVelocity)
-                    }
-                } else {
-                    scope.launch {
-                        state.snapTo(newAnchors.closestAnchor(offset = state.requireOffset()))
-                    }
+        val previousTargetOffset = previousAnchors[previousTarget]
+        val newTargetOffset = newAnchors[previousTarget]
+        if (previousTargetOffset != newTargetOffset) {
+            if (newTargetOffset != null) {
+                scope.launch {
+                    state.animateTo(previousTarget, state.lastVelocity)
+                }
+            } else {
+                scope.launch {
+                    state.snapTo(newAnchors.closestAnchor(offset = state.requireOffset()))
                 }
             }
         }
+    }
 }
 
 private fun <T> Map<T, Float>.closestAnchor(