diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 25529bba382f0..d8577c3ab3fc0 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -17,7 +17,11 @@ android_app {
static_libs: [
"androidx.activity_activity-compose",
"androidx.appcompat_appcompat",
- "androidx.compose.material_material",
+ "androidx.compose.animation_animation-core",
+ "androidx.compose.foundation_foundation",
+ "androidx.compose.material3_material3",
+ "androidx.compose.material_material-icons-core",
+ "androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling",
@@ -27,6 +31,7 @@ android_app {
"androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-viewmodel-compose",
"androidx.recyclerview_recyclerview",
+ "kotlinx-coroutines-core",
],
platform_apis: true,
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 08ab1b4000f8f..6178efcf86c01 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -27,4 +27,6 @@
passwords
sign-ins
CreateOptionInfo credentialType icon
+
+ "Close sheet"
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt
new file mode 100644
index 0000000000000..f1f453da4f38d
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.common.material
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.android.credentialmanager.R
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.Expanded
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.HalfExpanded
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.Hidden
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.launch
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ * Possible values of [ModalBottomSheetState].
+ */
+enum class ModalBottomSheetValue {
+ /**
+ * The bottom sheet is not visible.
+ */
+ Hidden,
+
+ /**
+ * The bottom sheet is visible at full height.
+ */
+ Expanded,
+
+ /**
+ * The bottom sheet is partially visible at 50% of the screen height. This state is only
+ * enabled if the height of the bottom sheet is more than 50% of the screen height.
+ */
+ HalfExpanded
+}
+
+/**
+ * State of the [ModalBottomSheetLayout] composable.
+ *
+ * @param initialValue The initial value of the state. Must not be set to
+ * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+class ModalBottomSheetState(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ internal val isSkipHalfExpanded: Boolean,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+) : SwipeableState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange
+) {
+ /**
+ * Whether the bottom sheet is visible.
+ */
+ val isVisible: Boolean
+ get() = currentValue != Hidden
+
+ internal val hasHalfExpandedState: Boolean
+ get() = anchors.values.contains(HalfExpanded)
+
+ constructor(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+ ) : this(initialValue, animationSpec, isSkipHalfExpanded = false, confirmStateChange)
+
+ init {
+ if (isSkipHalfExpanded) {
+ require(initialValue != HalfExpanded) {
+ "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" +
+ " true."
+ }
+ }
+ }
+
+ /**
+ * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller
+ * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be
+ * fully expanded.
+ *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ suspend fun show() {
+ val targetValue = when {
+ hasHalfExpandedState -> HalfExpanded
+ else -> Expanded
+ }
+ animateTo(targetValue = targetValue)
+ }
+
+ /**
+ * Half expand the bottom sheet if half expand is enabled with animation and suspend until it
+ * animation is complete or cancelled
+ *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ internal suspend fun halfExpand() {
+ if (!hasHalfExpandedState) {
+ return
+ }
+ animateTo(HalfExpanded)
+ }
+
+ /**
+ * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
+ * animation has been cancelled.
+ * *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ internal suspend fun expand() = animateTo(Expanded)
+
+ /**
+ * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
+ * been cancelled.
+ *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ suspend fun hide() = animateTo(Hidden)
+
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [ModalBottomSheetState].
+ */
+ fun Saver(
+ animationSpec: AnimationSpec,
+ skipHalfExpanded: Boolean,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean
+ ): Saver = Saver(
+ save = { it.currentValue },
+ restore = {
+ ModalBottomSheetState(
+ initialValue = it,
+ animationSpec = animationSpec,
+ isSkipHalfExpanded = skipHalfExpanded,
+ confirmStateChange = confirmStateChange
+ )
+ }
+ )
+
+ /**
+ * The default [Saver] implementation for [ModalBottomSheetState].
+ */
+ @Deprecated(
+ message = "Please specify the skipHalfExpanded parameter",
+ replaceWith = ReplaceWith(
+ "ModalBottomSheetState.Saver(" +
+ "animationSpec = animationSpec," +
+ "skipHalfExpanded = ," +
+ "confirmStateChange = confirmStateChange" +
+ ")"
+ )
+ )
+ fun Saver(
+ animationSpec: AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean
+ ): Saver = Saver(
+ animationSpec = animationSpec,
+ skipHalfExpanded = false,
+ confirmStateChange = confirmStateChange
+ )
+ }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun rememberModalBottomSheetState(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ skipHalfExpanded: Boolean,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState {
+ return rememberSaveable(
+ initialValue, animationSpec, skipHalfExpanded, confirmStateChange,
+ saver = ModalBottomSheetState.Saver(
+ animationSpec = animationSpec,
+ skipHalfExpanded = skipHalfExpanded,
+ confirmStateChange = confirmStateChange
+ )
+ ) {
+ ModalBottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ isSkipHalfExpanded = skipHalfExpanded,
+ confirmStateChange = confirmStateChange
+ )
+ }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun rememberModalBottomSheetState(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState = rememberModalBottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ skipHalfExpanded = false,
+ confirmStateChange = confirmStateChange
+)
+
+/**
+ * Material Design modal bottom sheet.
+ *
+ * Modal bottom sheets present a set of choices while blocking interaction with the rest of the
+ * screen. They are an alternative to inline menus and simple dialogs, providing
+ * additional room for content, iconography, and actions.
+ *
+ * 
+ *
+ * A simple example of a modal bottom sheet looks like this:
+ *
+ * @sample androidx.compose.material.samples.ModalBottomSheetSample
+ *
+ * @param sheetContent The content of the bottom sheet.
+ * @param modifier Optional [Modifier] for the entire component.
+ * @param sheetState The state of the bottom sheet.
+ * @param sheetShape The shape of the bottom sheet.
+ * @param sheetElevation The elevation of the bottom sheet.
+ * @param sheetBackgroundColor The background color of the bottom sheet.
+ * @param sheetContentColor The preferred content color provided by the bottom sheet to its
+ * children. Defaults to the matching content color for [sheetBackgroundColor], or if that is not
+ * a color from the theme, this will keep the same content color set above the bottom sheet.
+ * @param scrimColor The color of the scrim that is applied to the rest of the screen when the
+ * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no
+ * longer be applied and the bottom sheet will not block interaction with the rest of the screen
+ * when visible.
+ * @param content The content of rest of the screen.
+ */
+@Composable
+fun ModalBottomSheetLayout(
+ sheetContent: @Composable ColumnScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ sheetState: ModalBottomSheetState =
+ rememberModalBottomSheetState(Hidden),
+ sheetShape: Shape = MaterialTheme.shapes.large,
+ sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+ sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
+ sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+ scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+ content: @Composable () -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ BoxWithConstraints(modifier) {
+ val fullHeight = constraints.maxHeight.toFloat()
+ val sheetHeightState = remember { mutableStateOf(null) }
+ Box(Modifier.fillMaxSize()) {
+ content()
+ Scrim(
+ color = scrimColor,
+ onDismiss = {
+ if (sheetState.confirmStateChange(Hidden)) {
+ scope.launch { sheetState.hide() }
+ }
+ },
+ visible = sheetState.targetValue != Hidden
+ )
+ }
+ Surface(
+ Modifier
+ .fillMaxWidth()
+ .nestedScroll(sheetState.nestedScrollConnection)
+ .offset {
+ val y = if (sheetState.anchors.isEmpty()) {
+ // if we don't know our anchors yet, render the sheet as hidden
+ fullHeight.roundToInt()
+ } else {
+ // if we do know our anchors, respect them
+ sheetState.offset.value.roundToInt()
+ }
+ IntOffset(0, y)
+ }
+ .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
+ .onGloballyPositioned {
+ sheetHeightState.value = it.size.height.toFloat()
+ }
+ .semantics {
+ if (sheetState.isVisible) {
+ dismiss {
+ if (sheetState.confirmStateChange(Hidden)) {
+ scope.launch { sheetState.hide() }
+ }
+ true
+ }
+ if (sheetState.currentValue == HalfExpanded) {
+ expand {
+ if (sheetState.confirmStateChange(Expanded)) {
+ scope.launch { sheetState.expand() }
+ }
+ true
+ }
+ } else if (sheetState.hasHalfExpandedState) {
+ collapse {
+ if (sheetState.confirmStateChange(HalfExpanded)) {
+ scope.launch { sheetState.halfExpand() }
+ }
+ true
+ }
+ }
+ }
+ },
+ shape = sheetShape,
+ shadowElevation = sheetElevation,
+ color = sheetBackgroundColor,
+ contentColor = sheetContentColor
+ ) {
+ Column(content = sheetContent)
+ }
+ }
+}
+
+@Suppress("ModifierInspectorInfo")
+private fun Modifier.bottomSheetSwipeable(
+ sheetState: ModalBottomSheetState,
+ fullHeight: Float,
+ sheetHeightState: State
+): Modifier {
+ val sheetHeight = sheetHeightState.value
+ val modifier = if (sheetHeight != null) {
+ val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) {
+ mapOf(
+ fullHeight to Hidden,
+ fullHeight - sheetHeight to Expanded
+ )
+ } else {
+ mapOf(
+ fullHeight to Hidden,
+ fullHeight / 2 to HalfExpanded,
+ max(0f, fullHeight - sheetHeight) to Expanded
+ )
+ }
+ Modifier.swipeable(
+ state = sheetState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = sheetState.currentValue != Hidden,
+ resistance = null
+ )
+ } else {
+ Modifier
+ }
+
+ return this.then(modifier)
+}
+
+@Composable
+private fun Scrim(
+ color: Color,
+ onDismiss: () -> Unit,
+ visible: Boolean
+) {
+ if (color.isSpecified) {
+ val alpha by animateFloatAsState(
+ targetValue = if (visible) 1f else 0f,
+ animationSpec = TweenSpec()
+ )
+ LocalConfiguration.current
+ val resources = LocalContext.current.resources
+ val closeSheet = resources.getString(R.string.close_sheet)
+ val dismissModifier = if (visible) {
+ Modifier
+ .pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
+ .semantics(mergeDescendants = true) {
+ contentDescription = closeSheet
+ onClick { onDismiss(); true }
+ }
+ } else {
+ Modifier
+ }
+
+ Canvas(
+ Modifier
+ .fillMaxSize()
+ .then(dismissModifier)
+ ) {
+ drawRect(color = color, alpha = alpha)
+ }
+ }
+}
+
+/**
+ * Contains useful Defaults for [ModalBottomSheetLayout].
+ */
+object ModalBottomSheetDefaults {
+
+ /**
+ * The default elevation used by [ModalBottomSheetLayout].
+ */
+ val Elevation = 16.dp
+
+ /**
+ * The default scrim color used by [ModalBottomSheetLayout].
+ */
+ val scrimColor: Color
+ @Composable
+ get() = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f)
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt
new file mode 100644
index 0000000000000..3e2de8321006c
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt
@@ -0,0 +1,875 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.common.material
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import com.android.credentialmanager.common.material.SwipeableDefaults.AnimationSpec
+import com.android.credentialmanager.common.material.SwipeableDefaults.StandardResistanceFactor
+import com.android.credentialmanager.common.material.SwipeableDefaults.VelocityThreshold
+import com.android.credentialmanager.common.material.SwipeableDefaults.resistanceConfig
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.sign
+import kotlin.math.sin
+
+/**
+ * State of the [swipeable] modifier.
+ *
+ * This contains necessary information about any ongoing swipe or animation and provides methods
+ * to change the state either immediately or by starting an animation. To create and remember a
+ * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Stable
+open class SwipeableState(
+ initialValue: T,
+ internal val animationSpec: AnimationSpec = AnimationSpec,
+ internal val confirmStateChange: (newValue: T) -> Boolean = { true }
+) {
+ /**
+ * The current value of the state.
+ *
+ * If no swipe or animation is in progress, this corresponds to the anchor at which the
+ * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
+ * the last anchor at which the [swipeable] was settled before the swipe or animation started.
+ */
+ var currentValue: T by mutableStateOf(initialValue)
+ private set
+
+ /**
+ * Whether the state is currently animating.
+ */
+ var isAnimationRunning: Boolean by mutableStateOf(false)
+ private set
+
+ /**
+ * The current position (in pixels) of the [swipeable].
+ *
+ * You should use this state to offset your content accordingly. The recommended way is to
+ * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
+ */
+ val offset: State get() = offsetState
+
+ /**
+ * The amount by which the [swipeable] has been swiped past its bounds.
+ */
+ val overflow: State get() = overflowState
+
+ // Use `Float.NaN` as a placeholder while the state is uninitialised.
+ private val offsetState = mutableStateOf(0f)
+ private val overflowState = mutableStateOf(0f)
+
+ // the source of truth for the "real"(non ui) position
+ // basically position in bounds + overflow
+ private val absoluteOffset = mutableStateOf(0f)
+
+ // current animation target, if animating, otherwise null
+ private val animationTarget = mutableStateOf(null)
+
+ internal var anchors by mutableStateOf(emptyMap())
+
+ private val latestNonEmptyAnchorsFlow: Flow