Merge "Updated docs-public/build.gradle for the following artifacts:" into androidx-main
diff --git a/.github/workflows/validate_gradle_wrapper.yml b/.github/workflows/validate_gradle_wrapper.yml
new file mode 100644
index 0000000..2e9a731
--- /dev/null
+++ b/.github/workflows/validate_gradle_wrapper.yml
@@ -0,0 +1,16 @@
+name: "Validate Gradle Wrapper"
+
+on: 
+  pull_request:
+    paths:
+    - 'gradlew'
+    - 'gradlew.bat'
+    - 'gradle/wrapper/'
+
+jobs:
+  validation:
+    name: "Validation"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: gradle/wrapper-validation-action@v1
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 434a88d..9350274 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -44,6 +44,7 @@
 import androidx.compose.animation.demos.lookahead.LookaheadWithDisappearingMovableContentDemo
 import androidx.compose.animation.demos.lookahead.LookaheadWithFlowRowDemo
 import androidx.compose.animation.demos.lookahead.LookaheadWithIntrinsicsDemo
+import androidx.compose.animation.demos.lookahead.LookaheadWithLazyColumn
 import androidx.compose.animation.demos.lookahead.LookaheadWithMovableContentDemo
 import androidx.compose.animation.demos.lookahead.LookaheadWithScaffold
 import androidx.compose.animation.demos.lookahead.LookaheadWithSubcompose
@@ -126,6 +127,7 @@
                     LookaheadWithBoxWithConstraints()
                 },
                 ComposableDemo("Lookahead With Subcompose") { LookaheadWithSubcompose() },
+                ComposableDemo("Lookahead With LazyColumn") { LookaheadWithLazyColumn() },
                 ComposableDemo("Lookahead With Flow Row") { LookaheadWithFlowRowDemo() },
                 ComposableDemo("Lookahead With Intrinsics") {
                     LookaheadWithIntrinsicsDemo()
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimateItemPlacement.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimateItemPlacement.kt
new file mode 100644
index 0000000..493556a
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimateItemPlacement.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.demos.layoutanimation.turquoiseColors
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
+@Preview
+@Composable
+fun LookaheadWithLAnimateItemPlacement() {
+    val visible by produceState(true) {
+        while (true) {
+            delay(2000)
+            value = !value
+        }
+    }
+    LookaheadScope {
+        LazyColumn(Modifier.padding(20.dp)) {
+            items(3, key = { it }) {
+                Column(
+                    Modifier
+                        .animateItemPlacement()
+                        .clip(RoundedCornerShape(15.dp))
+                        .background(turquoiseColors[it])
+
+                ) {
+                    Box(
+                        Modifier
+                            .requiredHeight(ItemSize.dp)
+                            .fillMaxWidth()
+                    )
+                    AnimatedVisibility(visible = visible) {
+                        Box(
+                            Modifier
+                                .requiredHeight(ItemSize.dp)
+                                .fillMaxWidth()
+                                .background(Color.White)
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Suppress("ConstPropertyName")
+private const val ItemSize = 100
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt
new file mode 100644
index 0000000..8fdbd52
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2023 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.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.demos.R
+import androidx.compose.animation.demos.gesture.pastelColors
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun LookaheadWithLazyColumn() {
+    LookaheadScope {
+        LazyColumn {
+            items(10, key = { it }) {
+                val index = it % 4
+                var expanded by rememberSaveable { mutableStateOf(false) }
+                AnimatedVisibility(
+                    remember { MutableTransitionState(false) }
+                        .apply { targetState = true },
+                    enter = slideInHorizontally { 20 } + fadeIn()
+                ) {
+                    Surface(shape = RoundedCornerShape(10.dp),
+                        color = pastelColors[index],
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .clickable {
+                                expanded = !expanded
+                            }) {
+                        LookaheadScope {
+                            val title = remember {
+                                movableContentOf {
+                                    Text(
+                                        names[index],
+                                        Modifier
+                                            .padding(20.dp)
+                                            .animateBounds(Modifier)
+                                    )
+                                }
+                            }
+                            val image = remember {
+                                if (index < 3) {
+                                    movableContentOf {
+                                        Image(
+                                            painter = painterResource(res[index]),
+                                            contentDescription = null,
+                                            modifier = Modifier
+                                                .padding(10.dp)
+                                                .animateBounds(
+                                                    if (expanded)
+                                                        Modifier.fillMaxWidth()
+                                                    else
+                                                        Modifier.size(80.dp),
+                                                    spring(stiffness = Spring.StiffnessLow)
+                                                )
+                                                .clip(RoundedCornerShape(5.dp)),
+                                            contentScale = if (expanded) {
+                                                ContentScale.FillWidth
+                                            } else {
+                                                ContentScale.Crop
+                                            }
+                                        )
+                                    }
+                                } else {
+                                    movableContentOf {
+                                        Box(
+                                            modifier = Modifier
+                                                .padding(10.dp)
+                                                .animateBounds(
+                                                    if (expanded) Modifier
+                                                        .fillMaxWidth()
+                                                        .aspectRatio(1f)
+                                                    else Modifier.size(80.dp),
+                                                    spring(stiffness = Spring.StiffnessLow)
+                                                )
+                                                .background(
+                                                    Color.LightGray, RoundedCornerShape(5.dp)
+                                                ),
+                                        )
+                                    }
+                                }
+                            }
+                            if (expanded) {
+                                Column {
+                                    title()
+                                    image()
+                                }
+                            } else {
+                                Row {
+                                    image()
+                                    title()
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+val names = listOf("YT", "Pepper", "Waffle", "Who?")
+val res = listOf(
+    R.drawable.yt_profile,
+    R.drawable.pepper,
+    R.drawable.waffle,
+)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg
new file mode 100644
index 0000000..1aa03c5
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/waffle.jpeg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/waffle.jpeg
new file mode 100644
index 0000000..194b4e5
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/waffle.jpeg
Binary files differ
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 035d8aa..e0d2b40 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -40,9 +40,9 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
@@ -864,7 +864,8 @@
 
     val graphicsLayerBlock = createGraphicsLayerBlock(enter, exit, label)
 
-    return (if (disableClip) Modifier else Modifier.clipToBounds())
+    return Modifier
+        .graphicsLayer(clip = !disableClip)
         .then(
             EnterExitTransitionElement(
                 this, sizeAnimation, offsetAnimation, slideAnimation,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
index 2763637..416fc19 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -49,7 +49,9 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -86,6 +88,7 @@
 
     private val isVertical: Boolean get() = config.isVertical
     private val reverseLayout: Boolean get() = config.reverseLayout
+    private val isInLookaheadScope: Boolean get() = config.isInLookaheadScope
 
     @get:Rule
     val rule = createComposeRule()
@@ -1842,6 +1845,7 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
     @Composable
     private fun LazyList(
         arrangement: Arrangement.HorizontalOrVertical? = null,
@@ -1854,63 +1858,70 @@
         endPadding: Dp = 0.dp,
         content: LazyListScope.() -> Unit
     ) {
-        state = rememberLazyListState(startIndex)
-        if (isVertical) {
-            val verticalArrangement =
-                arrangement ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom
-            val horizontalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
-                Alignment.Start
-            } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
-                Alignment.CenterHorizontally
-            } else {
-                Alignment.End
-            }
-            LazyColumn(
-                state = state,
-                modifier = Modifier
-                    .requiredHeightIn(min = minSize, max = maxSize)
-                    .then(
-                        if (crossAxisSize != Dp.Unspecified) {
-                            Modifier.requiredWidth(crossAxisSize)
-                        } else {
-                            Modifier.fillMaxWidth()
-                        }
-                    )
-                    .testTag(ContainerTag),
-                verticalArrangement = verticalArrangement,
-                horizontalAlignment = horizontalAlignment,
-                reverseLayout = reverseLayout,
-                contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
-                content = content
-            )
+        val container: @Composable (@Composable () -> Unit) -> Unit = if (isInLookaheadScope) {
+            { LookaheadScope { it() } }
         } else {
-            val horizontalArrangement =
-                arrangement ?: if (!reverseLayout) Arrangement.Start else Arrangement.End
-            val verticalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
-                Alignment.Top
-            } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
-                Alignment.CenterVertically
+            { it() }
+        }
+        container {
+            state = rememberLazyListState(startIndex)
+            if (isVertical) {
+                val verticalArrangement =
+                    arrangement ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom
+                val horizontalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
+                    Alignment.Start
+                } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
+                    Alignment.CenterHorizontally
+                } else {
+                    Alignment.End
+                }
+                LazyColumn(
+                    state = state,
+                    modifier = Modifier
+                        .requiredHeightIn(min = minSize, max = maxSize)
+                        .then(
+                            if (crossAxisSize != Dp.Unspecified) {
+                                Modifier.requiredWidth(crossAxisSize)
+                            } else {
+                                Modifier.fillMaxWidth()
+                            }
+                        )
+                        .testTag(ContainerTag),
+                    verticalArrangement = verticalArrangement,
+                    horizontalAlignment = horizontalAlignment,
+                    reverseLayout = reverseLayout,
+                    contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
+                    content = content
+                )
             } else {
-                Alignment.Bottom
+                val horizontalArrangement =
+                    arrangement ?: if (!reverseLayout) Arrangement.Start else Arrangement.End
+                val verticalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
+                    Alignment.Top
+                } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
+                    Alignment.CenterVertically
+                } else {
+                    Alignment.Bottom
+                }
+                LazyRow(
+                    state = state,
+                    modifier = Modifier
+                        .requiredWidthIn(min = minSize, max = maxSize)
+                        .then(
+                            if (crossAxisSize != Dp.Unspecified) {
+                                Modifier.requiredHeight(crossAxisSize)
+                            } else {
+                                Modifier.fillMaxHeight()
+                            }
+                        )
+                        .testTag(ContainerTag),
+                    horizontalArrangement = horizontalArrangement,
+                    verticalAlignment = verticalAlignment,
+                    reverseLayout = reverseLayout,
+                    contentPadding = PaddingValues(start = startPadding, end = endPadding),
+                    content = content
+                )
             }
-            LazyRow(
-                state = state,
-                modifier = Modifier
-                    .requiredWidthIn(min = minSize, max = maxSize)
-                    .then(
-                        if (crossAxisSize != Dp.Unspecified) {
-                            Modifier.requiredHeight(crossAxisSize)
-                        } else {
-                            Modifier.fillMaxHeight()
-                        }
-                    )
-                    .testTag(ContainerTag),
-                horizontalArrangement = horizontalArrangement,
-                verticalAlignment = verticalAlignment,
-                reverseLayout = reverseLayout,
-                contentPadding = PaddingValues(start = startPadding, end = endPadding),
-                content = content
-            )
         }
     }
 
@@ -1952,19 +1963,22 @@
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
         fun params() = arrayOf(
-            Config(isVertical = true, reverseLayout = false),
-            Config(isVertical = false, reverseLayout = false),
-            Config(isVertical = true, reverseLayout = true),
-            Config(isVertical = false, reverseLayout = true),
+            Config(isVertical = true, reverseLayout = false, isInLookaheadScope = false),
+            Config(isVertical = false, reverseLayout = false, isInLookaheadScope = false),
+            Config(isVertical = true, reverseLayout = true, isInLookaheadScope = false),
+            Config(isVertical = false, reverseLayout = true, isInLookaheadScope = false),
+            Config(isVertical = true, reverseLayout = false, isInLookaheadScope = true)
         )
 
         class Config(
             val isVertical: Boolean,
-            val reverseLayout: Boolean
+            val reverseLayout: Boolean,
+            val isInLookaheadScope: Boolean
         ) {
             override fun toString() =
                 (if (isVertical) "LazyColumn" else "LazyRow") +
-                    (if (reverseLayout) "(reverse)" else "")
+                    (if (reverseLayout) "(reverse)" else "") +
+                    (if (isInLookaheadScope) "(in LookaheadScope)" else "")
         }
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 2b0b778..19191a5 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -17,7 +17,12 @@
 package androidx.compose.foundation.lazy.list
 
 import android.os.Build
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.VelocityTrackerCalculationThreshold
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
@@ -25,6 +30,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -44,6 +50,7 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.matchers.isZero
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
@@ -54,6 +61,7 @@
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.draw.drawBehind
@@ -67,7 +75,11 @@
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.findRootCoordinates
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -106,6 +118,7 @@
 import java.util.concurrent.CountDownLatch
 import kotlin.math.abs
 import kotlin.math.roundToInt
+import kotlin.test.assertEquals
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -2171,6 +2184,640 @@
     }
 
     @Test
+    fun testLookaheadPositionWithOnlyInBoundChanges() {
+        testLookaheadPositionWithPlacementAnimator(
+            initialList = listOf(0, 1, 2, 3),
+            targetList = listOf(3, 2, 1, 0),
+            initialExpectedLookaheadPositions = listOf(0, 100, 200, 300),
+            targetExpectedLookaheadPositions = listOf(300, 200, 100, 0)
+        )
+    }
+
+    @Test
+    fun testLookaheadPositionWithCustomStartingIndex() {
+        testLookaheadPositionWithPlacementAnimator(
+            initialList = listOf(0, 1, 2, 3, 4),
+            targetList = listOf(4, 3, 2, 1, 0),
+            initialExpectedLookaheadPositions = listOf(null, 0, 100, 200, 300),
+            targetExpectedLookaheadPositions = listOf(300, 200, 100, 0, -100),
+            startingIndex = 1
+        )
+    }
+
+    @Test
+    fun testLookaheadPositionWithTwoInBoundTwoOutBound() {
+        testLookaheadPositionWithPlacementAnimator(
+            initialList = listOf(0, 1, 2, 3, 4, 5),
+            targetList = listOf(5, 4, 2, 1, 3, 0),
+            initialExpectedLookaheadPositions = listOf(null, null, 0, 100, 200, 300),
+            targetExpectedLookaheadPositions = listOf(300, 100, 0, 200, -100, -200),
+            startingIndex = 2
+        )
+    }
+
+    private fun testLookaheadPositionWithPlacementAnimator(
+        initialList: List<Int>,
+        targetList: List<Int>,
+        initialExpectedLookaheadPositions: List<Int?>,
+        targetExpectedLookaheadPositions: List<Int?>,
+        startingIndex: Int = 0
+    ) {
+        var list by mutableStateOf(initialList)
+        val lookaheadPosition = mutableMapOf<Int, Int>()
+        val postLookaheadPosition = mutableMapOf<Int, Int>()
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LazyListInLookaheadScope(
+                    list = list,
+                    startingIndex = startingIndex,
+                    lookaheadPosition = lookaheadPosition,
+                    postLookaheadPosition = postLookaheadPosition
+                )
+            }
+        }
+        rule.runOnIdle {
+            repeat(list.size) {
+                assertEquals(initialExpectedLookaheadPositions[it], lookaheadPosition[it])
+                assertEquals(initialExpectedLookaheadPositions[it], postLookaheadPosition[it])
+            }
+            lookaheadPosition.clear()
+            postLookaheadPosition.clear()
+            list = targetList
+        }
+        rule.waitForIdle()
+        repeat(20) {
+            rule.mainClock.advanceTimeByFrame()
+            repeat(list.size) {
+                assertEquals(targetExpectedLookaheadPositions[it], lookaheadPosition[it])
+            }
+        }
+        repeat(list.size) {
+            if (lookaheadPosition[it]?.let { offset -> offset + ItemSize >= 0 } != false) {
+                assertEquals(lookaheadPosition[it], postLookaheadPosition[it])
+            }
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+    @Composable
+    private fun LazyListInLookaheadScope(
+        list: List<Int>,
+        startingIndex: Int,
+        lookaheadPosition: MutableMap<Int, Int>,
+        postLookaheadPosition: MutableMap<Int, Int>
+    ) {
+        LookaheadScope {
+            LazyColumnOrRow(
+                if (vertical) {
+                    Modifier.requiredHeight(ItemSize.dp * (list.size - startingIndex))
+                } else {
+                    Modifier.requiredWidth(ItemSize.dp * (list.size - startingIndex))
+                },
+                state = rememberLazyListState(
+                    initialFirstVisibleItemIndex = startingIndex
+                ),
+
+                ) {
+                items(list, key = { it }) { item ->
+                    Box(
+                        Modifier
+                            .animateItemPlacement(tween(160))
+                            .trackPositions(
+                                lookaheadPosition,
+                                postLookaheadPosition,
+                                this@LookaheadScope,
+                                item
+                            )
+                            .requiredSize(ItemSize.dp)
+                    )
+                }
+            }
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    private fun Modifier.trackPositions(
+        lookaheadPosition: MutableMap<Int, Int>,
+        postLookaheadPosition: MutableMap<Int, Int>,
+        lookaheadScope: LookaheadScope,
+        item: Int
+    ): Modifier = this.layout { measurable, constraints ->
+        measurable
+            .measure(constraints)
+            .run {
+                layout(width, height) {
+                    if (isLookingAhead) {
+                        lookaheadPosition[item] =
+                            with(lookaheadScope) {
+                                coordinates!!
+                                    .findRootCoordinates()
+                                    .localLookaheadPositionOf(
+                                        coordinates!!
+                                    )
+                                    .let {
+                                        if (vertical) {
+                                            it.y
+                                        } else {
+                                            it.x
+                                        }.roundToInt()
+                                    }
+                            }
+                    } else {
+                        postLookaheadPosition[item] =
+                            coordinates!!
+                                .positionInRoot()
+                                .let {
+                                    if (vertical) it.y else it.x
+                                }
+                                .roundToInt()
+                    }
+                    place(0, 0)
+                }
+            }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+    @Test
+    fun animContentSizeWithPlacementAnimator() {
+        val lookaheadPosition = mutableMapOf<Int, Int>()
+        val postLookaheadPosition = mutableMapOf<Int, Int>()
+        var large by mutableStateOf(false)
+        var animateSizeChange by mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    LazyColumnOrRow {
+                        items(4, key = { it }) {
+                            Box(
+                                Modifier
+                                    .animateItemPlacement(tween(160, easing = LinearEasing))
+                                    .trackPositions(
+                                        lookaheadPosition,
+                                        postLookaheadPosition,
+                                        this@LookaheadScope,
+                                        it
+                                    )
+                                    .then(
+                                        if (animateSizeChange) Modifier.animateContentSize(
+                                            tween(160)
+                                        ) else Modifier
+                                    )
+                                    .requiredSize(if (large) ItemSize.dp * 2 else ItemSize.dp)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        repeat(4) {
+            assertEquals(it * ItemSize, lookaheadPosition[it])
+            assertEquals(it * ItemSize, postLookaheadPosition[it])
+        }
+
+        rule.mainClock.autoAdvance = false
+        large = true
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        repeat(20) { frame ->
+            val fraction = (frame * 16 / 160f).coerceAtMost(1f)
+            repeat(4) {
+                assertEquals(it * ItemSize * 2, lookaheadPosition[it])
+                assertEquals(
+                    (it * ItemSize * (1 + fraction)).roundToInt(),
+                    postLookaheadPosition[it]
+                )
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Enable animateContentSize
+        animateSizeChange = true
+        large = false
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        repeat(20) { frame ->
+            val fraction = (frame * 16 / 160f).coerceAtMost(1f)
+            repeat(4) {
+                // Verify that item target offsets are not affected by animateContentSize
+                assertEquals(it * ItemSize, lookaheadPosition[it])
+                assertEquals(
+                    (it * (2 - fraction) * ItemSize).roundToInt(),
+                    postLookaheadPosition[it]
+                )
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+    @Test
+    fun animVisibilityWithPlacementAnimator() {
+        val lookaheadPosition = mutableMapOf<Int, Int>()
+        val postLookaheadPosition = mutableMapOf<Int, Int>()
+        var visible by mutableStateOf(false)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    LazyColumnOrRow {
+                        items(4, key = { it }) {
+                            if (vertical) {
+                                Column(
+                                    Modifier
+                                        .animateItemPlacement(tween(160, easing = LinearEasing))
+                                        .trackPositions(
+                                            lookaheadPosition,
+                                            postLookaheadPosition,
+                                            this@LookaheadScope,
+                                            it
+                                        )
+                                ) {
+                                    Box(Modifier.requiredSize(ItemSize.dp))
+                                    AnimatedVisibility(visible = visible) {
+                                        Box(Modifier.requiredSize(ItemSize.dp))
+                                    }
+                                }
+                            } else {
+                                Row(
+                                    Modifier
+                                        .animateItemPlacement(tween(160, easing = LinearEasing))
+                                        .trackPositions(
+                                            lookaheadPosition,
+                                            postLookaheadPosition,
+                                            this@LookaheadScope,
+                                            it
+                                        )
+                                ) {
+                                    Box(Modifier.requiredSize(ItemSize.dp))
+                                    AnimatedVisibility(visible = visible) {
+                                        Box(Modifier.requiredSize(ItemSize.dp))
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        repeat(4) {
+            assertEquals(it * ItemSize, lookaheadPosition[it])
+            assertEquals(it * ItemSize, postLookaheadPosition[it])
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = true
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        repeat(20) { frame ->
+            val fraction = (frame * 16 / 160f).coerceAtMost(1f)
+            repeat(4) {
+                assertEquals(it * ItemSize * 2, lookaheadPosition[it])
+                assertEquals(
+                    (it * ItemSize * (1 + fraction)).roundToInt(),
+                    postLookaheadPosition[it]
+                )
+            }
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun resizeLazyList() {
+        val lookaheadPositions = mutableMapOf<Int, Offset>()
+        val postLookaheadPositions = mutableMapOf<Int, Offset>()
+        var postLookaheadSize by mutableStateOf(ItemSize * 2)
+        rule.setContent {
+            LookaheadScope {
+                CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                    LazyColumnOrRow(Modifier.layout { measurable, _ ->
+                        val constraints = if (isLookingAhead) {
+                            Constraints.fixed(4 * ItemSize, 4 * ItemSize)
+                        } else {
+                            Constraints.fixed(postLookaheadSize, postLookaheadSize)
+                        }
+                        measurable.measure(constraints).run {
+                            layout(width, height) {
+                                place(0, 0)
+                            }
+                        }
+                    }) {
+                        items(4) {
+                            Box(
+                                Modifier
+                                    .requiredSize(ItemSize.dp)
+                                    .layout { measurable, constraints ->
+                                        measurable
+                                            .measure(constraints)
+                                            .run {
+                                                layout(width, height) {
+                                                    if (isLookingAhead) {
+                                                        lookaheadPositions[it] = coordinates!!
+                                                            .findRootCoordinates()
+                                                            .localLookaheadPositionOf(coordinates!!)
+                                                    } else {
+                                                        postLookaheadPositions[it] =
+                                                            coordinates!!.positionInRoot()
+                                                    }
+                                                }
+                                            }
+                                    })
+                        }
+                    }
+                }
+            }
+        }
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+        postLookaheadSize = (2.9f * ItemSize).toInt()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+        postLookaheadSize = (3.4f * ItemSize).toInt()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(ItemSize * 3, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+
+        // Shrinking post-lookahead size
+        postLookaheadSize = (2.7f * ItemSize).toInt()
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+
+        // Shrinking post-lookahead size
+        postLookaheadSize = (1.2f * ItemSize).toInt()
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun lookaheadSizeSmallerThanPostLookahead() {
+        val lookaheadPositions = mutableMapOf<Int, Offset>()
+        val postLookaheadPositions = mutableMapOf<Int, Offset>()
+        var lookaheadSize by mutableStateOf(ItemSize * 2)
+        var postLookaheadSize by mutableStateOf(ItemSize * 4)
+        rule.setContent {
+            LookaheadScope {
+                CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                    LazyColumnOrRow(Modifier.layout { measurable, _ ->
+                        val constraints = if (isLookingAhead) {
+                            Constraints.fixed(lookaheadSize, lookaheadSize)
+                        } else {
+                            Constraints.fixed(postLookaheadSize, postLookaheadSize)
+                        }
+                        measurable.measure(constraints).run {
+                            layout(width, height) {
+                                place(0, 0)
+                            }
+                        }
+                    }) {
+                        items(4) {
+                            Box(
+                                Modifier
+                                    .requiredSize(ItemSize.dp)
+                                    .layout { measurable, constraints ->
+                                        measurable
+                                            .measure(constraints)
+                                            .run {
+                                                layout(width, height) {
+                                                    if (isLookingAhead) {
+                                                        lookaheadPositions[it] = coordinates!!
+                                                            .findRootCoordinates()
+                                                            .localLookaheadPositionOf(coordinates!!)
+                                                    } else {
+                                                        postLookaheadPositions[it] =
+                                                            coordinates!!.positionInRoot()
+                                                    }
+                                                }
+                                            }
+                                    })
+                        }
+                    }
+                }
+            }
+        }
+        // postLookaheadSize was initialized to 4 * ItemSize
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(ItemSize * 3, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+        postLookaheadSize = (2.9f * ItemSize).toInt()
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+        postLookaheadSize = 2 * ItemSize
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+
+        // Growing post-lookahead size
+        postLookaheadSize = (2.7f * ItemSize).toInt()
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(ItemSize * 2, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+
+        // Shrinking post-lookahead size
+        postLookaheadSize = (1.2f * ItemSize).toInt()
+        postLookaheadPositions.clear()
+        rule.runOnIdle {
+            repeat(4) {
+                assertEquals(it * ItemSize, lookaheadPositions[it]?.mainAxisPosition)
+            }
+            assertEquals(0, postLookaheadPositions[0]?.mainAxisPosition)
+            assertEquals(ItemSize, postLookaheadPositions[1]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[2]?.mainAxisPosition)
+            assertEquals(null, postLookaheadPositions[3]?.mainAxisPosition)
+        }
+    }
+    private val Offset.mainAxisPosition get() = (if (vertical) y else x).roundToInt()
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun postLookaheadItemsComposed() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    state = rememberLazyListState()
+                    LazyColumnOrRow(Modifier.requiredSize(300.dp), state) {
+                        items(12, key = { it }) {
+                            Box(
+                                Modifier
+                                    .testTag("$it")
+                                    .then(
+                                        if (it == 0) {
+                                            Modifier.layout { measurable, constraints ->
+                                                val p = measurable.measure(constraints)
+                                                val size = if (isLookingAhead) 300 else 30
+                                                layout(size, size) {
+                                                    p.place(0, 0)
+                                                }
+                                            }
+                                        } else
+                                            Modifier.size(30.dp)
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // Based on lookahead item 0 would be the only item needed, but post-lookahead calculation
+        // indicates 10 items will be needed to fill the viewport.
+        for (i in 0 until 10) {
+            rule.onNodeWithTag("$i")
+                .assertIsPlaced()
+        }
+        for (i in 10 until 12) {
+            rule.onNodeWithTag("$i")
+                .assertDoesNotExist()
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun postLookaheadItemsComposedBasedOnScrollDelta() {
+        var lookaheadSize by mutableStateOf(30)
+        var postLookaheadSize by mutableStateOf(lookaheadSize)
+        lateinit var state: LazyListState
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    state = rememberLazyListState()
+                    LazyColumnOrRow(Modifier.requiredSize(300.dp), state) {
+                        items(12, key = { it }) {
+                            Box(
+                                Modifier
+                                    .testTag("$it")
+                                    .then(
+                                        if (it == 2) {
+                                            Modifier.layout { measurable, constraints ->
+                                                val p = measurable.measure(constraints)
+                                                val size = if (isLookingAhead)
+                                                    lookaheadSize
+                                                else
+                                                    postLookaheadSize
+                                                layout(size, size) {
+                                                    p.place(0, 0)
+                                                }
+                                            }
+                                        } else
+                                            Modifier.size(30.dp)
+                                    )
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        for (i in 0 until 12) {
+            if (i < 10) {
+                rule.onNodeWithTag("$i")
+                    .assertIsPlaced()
+            } else {
+                rule.onNodeWithTag("$i")
+                    .assertDoesNotExist()
+            }
+        }
+
+        lookaheadSize = 300
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(60f)
+            }
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("0").assertIsNotPlaced()
+        rule.onNodeWithTag("1").assertIsNotPlaced()
+        for (i in 2 until 12) {
+            rule.onNodeWithTag("$i").assertIsPlaced()
+        }
+
+        postLookaheadSize = 300
+        for (i in 0 until 12) {
+            if (i == 2) {
+                rule.onNodeWithTag("$i").assertIsPlaced()
+            } else {
+                rule.onNodeWithTag("$i").assertIsNotPlaced()
+            }
+        }
+    }
+
+    @Test
     fun usingFillParentMaxSizeOnInfinityConstraintsIsIgnored() {
         rule.setContentWithTestViewConfiguration {
             Layout(content = {
@@ -2311,4 +2958,6 @@
                 endVelocity = 0f
             )
         }
-    }
\ No newline at end of file
+    }
+
+private const val ItemSize = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 6f41836..99f6a33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.overscroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -79,6 +80,8 @@
     val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
 
     val semanticState = rememberLazyListSemanticState(state, isVertical)
+    val scope = rememberCoroutineScope()
+    state.coroutineScope = scope
 
     val measurePolicy = rememberLazyListMeasurePolicy(
         itemProviderLambda,
@@ -182,6 +185,8 @@
     verticalArrangement
 ) {
     { containerConstraints ->
+        // Tracks if the lookahead pass has occurred
+        val hasLookaheadPassOccurred = state.hasLookaheadPassOccurred || isLookingAhead
         checkScrollableContainerConstraints(
             containerConstraints,
             if (isVertical) Orientation.Vertical else Orientation.Horizontal
@@ -307,6 +312,12 @@
             beyondBoundsInfo = state.beyondBoundsInfo
         )
 
+        val scrollToBeConsumed = if (isLookingAhead || !hasLookaheadPassOccurred) {
+            state.scrollToBeConsumed
+        } else {
+            state.scrollDeltaBetweenPasses
+        }
+
         measureLazyList(
             itemsCount = itemsCount,
             measuredItemProvider = measuredItemProvider,
@@ -316,7 +327,7 @@
             spaceBetweenItems = spaceBetweenItems,
             firstVisibleItemIndex = firstVisibleItemIndex,
             firstVisibleItemScrollOffset = firstVisibleScrollOffset,
-            scrollToBeConsumed = state.scrollToBeConsumed,
+            scrollToBeConsumed = scrollToBeConsumed,
             constraints = contentConstraints,
             isVertical = isVertical,
             headerIndexes = itemProvider.headerIndexes,
@@ -327,6 +338,9 @@
             placementAnimator = state.placementAnimator,
             beyondBoundsItemCount = beyondBoundsItemCount,
             pinnedItems = pinnedItems,
+            hasLookaheadPassOccurred = hasLookaheadPassOccurred,
+            isLookingAhead = isLookingAhead,
+            postLookaheadLayoutInfo = state.postLookaheadLayoutInfo,
             layout = { width, height, placement ->
                 layout(
                     containerConstraints.constrainWidth(width + totalHorizontalPadding),
@@ -336,7 +350,7 @@
                 )
             }
         ).also {
-            state.applyMeasureResult(it)
+            state.applyMeasureResult(it, isLookingAhead)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
index ec0fb82..7ad2893 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
@@ -56,7 +56,9 @@
         layoutHeight: Int,
         positionedItems: MutableList<LazyListMeasuredItem>,
         itemProvider: LazyListMeasuredItemProvider,
-        isVertical: Boolean
+        isVertical: Boolean,
+        isLookingAhead: Boolean,
+        hasLookaheadOccurred: Boolean
     ) {
         if (!positionedItems.fastAny { it.hasAnimations } && activeKeys.isEmpty()) {
             // no animations specified - no work needed
@@ -66,6 +68,7 @@
 
         val previousFirstVisibleIndex = firstVisibleIndex
         firstVisibleIndex = positionedItems.firstOrNull()?.index ?: 0
+
         val previousKeyToIndexMap = keyIndexMap
         keyIndexMap = itemProvider.keyIndexMap
 
@@ -78,6 +81,9 @@
             IntOffset(consumedScroll, 0)
         }
 
+        // Only setup animations when we have access to target value in the current pass, which
+        // means lookahead pass, or regular pass when not in a lookahead scope.
+        val shouldSetupAnimation = isLookingAhead || !hasLookaheadOccurred
         // first add all items we had in the previous run
         movingAwayKeys.addAll(activeKeys)
         // iterate through the items which are visible (without animated offsets)
@@ -102,12 +108,15 @@
                         )
                     }
                 } else {
-                    item.forEachNode { _, node ->
-                        if (node.rawOffset != LazyLayoutAnimateItemModifierNode.NotInitialized) {
-                            node.rawOffset += scrollOffset
+                    if (shouldSetupAnimation) {
+                        item.forEachNode { _, node ->
+                            if (node.rawOffset != LazyLayoutAnimateItemModifierNode.NotInitialized
+                            ) {
+                                node.rawOffset += scrollOffset
+                            }
                         }
+                        startAnimationsIfNeeded(item)
                     }
-                    startAnimationsIfNeeded(item)
                 }
             } else {
                 // no animation, clean up if needed
@@ -116,20 +125,22 @@
         }
 
         var accumulatedOffset = 0
-        movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
-        movingInFromStartBound.fastForEach { item ->
-            accumulatedOffset += item.size
-            val mainAxisOffset = 0 - accumulatedOffset
-            initializeNode(item, mainAxisOffset)
-            startAnimationsIfNeeded(item)
-        }
-        accumulatedOffset = 0
-        movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
-        movingInFromEndBound.fastForEach { item ->
-            val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
-            accumulatedOffset += item.size
-            initializeNode(item, mainAxisOffset)
-            startAnimationsIfNeeded(item)
+        if (shouldSetupAnimation) {
+            movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
+            movingInFromStartBound.fastForEach { item ->
+                accumulatedOffset += item.size
+                val mainAxisOffset = 0 - accumulatedOffset
+                initializeNode(item, mainAxisOffset)
+                startAnimationsIfNeeded(item)
+            }
+            accumulatedOffset = 0
+            movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
+            movingInFromEndBound.fastForEach { item ->
+                val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
+                accumulatedOffset += item.size
+                initializeNode(item, mainAxisOffset)
+                startAnimationsIfNeeded(item)
+            }
         }
 
         movingAwayKeys.forEach { key ->
@@ -168,9 +179,11 @@
             val mainAxisOffset = 0 - accumulatedOffset
 
             item.position(mainAxisOffset, layoutWidth, layoutHeight)
-            positionedItems.add(item)
-            startAnimationsIfNeeded(item)
+            if (shouldSetupAnimation) {
+                startAnimationsIfNeeded(item)
+            }
         }
+
         accumulatedOffset = 0
         movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
         movingAwayToEndBound.fastForEach { item ->
@@ -178,10 +191,16 @@
             accumulatedOffset += item.size
 
             item.position(mainAxisOffset, layoutWidth, layoutHeight)
-            positionedItems.add(item)
-            startAnimationsIfNeeded(item)
+            if (shouldSetupAnimation) {
+                startAnimationsIfNeeded(item)
+            }
         }
 
+        // This adds the new items to the list of positioned items while keeping the index of
+        // the positioned items sorted in ascending order.
+        positionedItems.addAll(0, movingAwayToStartBound.apply { reverse() })
+        positionedItems.addAll(movingAwayToEndBound)
+
         movingInFromStartBound.clear()
         movingInFromEndBound.clear()
         movingAwayToStartBound.clear()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index a352409..3a7dce6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -27,6 +27,8 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import kotlin.math.abs
 import kotlin.math.roundToInt
@@ -57,6 +59,9 @@
     placementAnimator: LazyListItemPlacementAnimator,
     beyondBoundsItemCount: Int,
     pinnedItems: List<Int>,
+    hasLookaheadPassOccurred: Boolean,
+    isLookingAhead: Boolean,
+    postLookaheadLayoutInfo: LazyListLayoutInfo?,
     @Suppress("PrimitiveInLambda")
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyListMeasureResult {
@@ -70,6 +75,7 @@
             canScrollForward = false,
             consumedScroll = 0f,
             measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+            scrollBackAmount = 0f,
             visibleItemsInfo = emptyList(),
             viewportStartOffset = -beforeContentPadding,
             viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
@@ -77,7 +83,7 @@
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
             afterContentPadding = afterContentPadding,
-            mainAxisItemSpacing = spaceBetweenItems
+            mainAxisItemSpacing = spaceBetweenItems,
         )
     } else {
         var currentFirstItemIndex = firstVisibleItemIndex
@@ -172,6 +178,7 @@
             index++
         }
 
+        val preScrollBackScrollDelta = scrollDelta
         // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
         // lets try to scroll back if we have enough items before firstVisibleItemIndex.
         if (currentMainAxisOffset < maxOffset) {
@@ -208,6 +215,16 @@
             scrollToBeConsumed
         }
 
+        val unconsumedScroll = scrollToBeConsumed - consumedScroll
+        // When scrolling to the bottom via gesture, there could be scrollback due to
+        // not being able to consume the whole scroll. In that case, the amount of
+        // scrollBack is the inverse of unconsumed scroll.
+        val scrollBackAmount: Float =
+            if (isLookingAhead && scrollDelta > preScrollBackScrollDelta && unconsumedScroll <= 0) {
+                scrollDelta - preScrollBackScrollDelta + unconsumedScroll
+            } else
+                0f
+
         // the initial offset for items from visibleItems list
         require(currentFirstItemScrollOffset >= 0) { "negative currentFirstItemScrollOffset" }
         val visibleItemsScrollOffset = -currentFirstItemScrollOffset
@@ -248,7 +265,10 @@
             measuredItemProvider = measuredItemProvider,
             itemsCount = itemsCount,
             beyondBoundsItemCount = beyondBoundsItemCount,
-            pinnedItems = pinnedItems
+            pinnedItems = pinnedItems,
+            consumedScroll = consumedScroll,
+            isLookingAhead = isLookingAhead,
+            lastPostLookaheadLayoutInfo = postLookaheadLayoutInfo
         )
 
         // Update maxCrossAxis with extra items
@@ -287,7 +307,9 @@
             layoutHeight = layoutHeight,
             positionedItems = positionedItems,
             itemProvider = measuredItemProvider,
-            isVertical = isVertical
+            isVertical = isVertical,
+            isLookingAhead = isLookingAhead,
+            hasLookaheadOccurred = hasLookaheadPassOccurred
         )
 
         val headerItem = if (headerIndexes.isNotEmpty()) {
@@ -311,18 +333,19 @@
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach {
                     if (it !== headerItem) {
-                        it.place(this)
+                        it.place(this, isLookingAhead)
                     }
                 }
                 // the header item should be placed (drawn) after all other items
-                headerItem?.place(this)
+                headerItem?.place(this, isLookingAhead)
             },
-            viewportStartOffset = -beforeContentPadding,
-            viewportEndOffset = maxOffset + afterContentPadding,
+            scrollBackAmount = scrollBackAmount,
             visibleItemsInfo = if (noExtraItems) positionedItems else positionedItems.fastFilter {
                 (it.index >= visibleItems.first().index && it.index <= visibleItems.last().index) ||
                     it === headerItem
             },
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = maxOffset + afterContentPadding,
             totalItemsCount = itemsCount,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
@@ -337,7 +360,10 @@
     measuredItemProvider: LazyListMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<Int>
+    pinnedItems: List<Int>,
+    consumedScroll: Float,
+    isLookingAhead: Boolean,
+    lastPostLookaheadLayoutInfo: LazyListLayoutInfo?
 ): List<LazyListMeasuredItem> {
     var list: MutableList<LazyListMeasuredItem>? = null
 
@@ -357,6 +383,60 @@
         }
     }
 
+    if (isLookingAhead) {
+        // Check if there's any item that needs to be composed based on last postLookaheadLayoutInfo
+        if (lastPostLookaheadLayoutInfo != null &&
+            lastPostLookaheadLayoutInfo.visibleItemsInfo.isNotEmpty()
+        ) {
+            // Find first item with index > end. Note that `visibleItemsInfo.last()` may not have
+            // the largest index as the last few items could be added to animate item placement.
+            val firstItem = lastPostLookaheadLayoutInfo.visibleItemsInfo.run {
+                var found: LazyListItemInfo? = null
+                for (i in size - 1 downTo 0) {
+                    if (this[i].index > end && (i == 0 || this[i - 1].index <= end)) {
+                        found = this[i]
+                        break
+                    }
+                }
+                found
+            }
+            val lastVisibleItem = lastPostLookaheadLayoutInfo.visibleItemsInfo.last()
+            if (firstItem != null) {
+                for (i in firstItem.index..lastVisibleItem.index) {
+                    if (list?.fastAny { it.index == i } != null) {
+                        if (list == null) list = mutableListOf()
+                        list?.add(measuredItemProvider.getAndMeasure(i))
+                    }
+                }
+            }
+
+            // Calculate the additional offset to subcompose based on what was shown in the
+            // previous post-loookahead pass and the scroll consumed.
+            val additionalOffset =
+                lastPostLookaheadLayoutInfo.viewportEndOffset - lastVisibleItem.offset -
+                    lastVisibleItem.size - consumedScroll
+            if (additionalOffset > 0) {
+                var index = lastVisibleItem.index + 1
+                var totalOffset = 0
+                while (index < itemsCount && totalOffset < additionalOffset) {
+                    val item = if (index <= end) {
+                        visibleItems.fastFirstOrNull { it.index == index }
+                    } else null
+                        ?: list?.fastFirstOrNull { it.index == index }
+                    if (item != null) {
+                        index++
+                        totalOffset += item.sizeWithSpacings
+                    } else {
+                        if (list == null) list = mutableListOf()
+                        list?.add(measuredItemProvider.getAndMeasure(index))
+                        index++
+                        totalOffset += list!!.last().sizeWithSpacings
+                    }
+                }
+            }
+        }
+    }
+
     return list ?: emptyList()
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
index d291659..9a7c9999 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -35,6 +35,8 @@
     val consumedScroll: Float,
     /** MeasureResult defining the layout.*/
     measureResult: MeasureResult,
+    /** The amount of scroll-back that happened due to reaching the end of the list. **/
+    val scrollBackAmount: Float,
     // properties representing the info needed for LazyListLayoutInfo:
     /** see [LazyListLayoutInfo.visibleItemsInfo] */
     override val visibleItemsInfo: List<LazyListItemInfo>,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
index b7993d7..70f38cc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateItemModifierNode
+import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateItemModifierNode.Companion.NotInitialized
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.IntOffset
@@ -134,6 +135,7 @@
 
     fun place(
         scope: Placeable.PlacementScope,
+        isLookingAhead: Boolean
     ) = with(scope) {
         require(mainAxisLayoutSize != Unset) { "position() should be called first" }
         repeat(placeablesCount) { index ->
@@ -143,14 +145,26 @@
             var offset = getOffset(index)
             val animateNode = getParentData(index) as? LazyLayoutAnimateItemModifierNode
             if (animateNode != null) {
-                val animatedOffset = offset + animateNode.placementDelta
-                // cancel the animation if current and target offsets are both out of the bounds.
-                if ((offset.mainAxis <= minOffset && animatedOffset.mainAxis <= minOffset) ||
-                    (offset.mainAxis >= maxOffset && animatedOffset.mainAxis >= maxOffset)
-                ) {
-                    animateNode.cancelAnimation()
+                if (isLookingAhead) {
+                    // Skip animation in lookahead pass
+                    animateNode.lookaheadOffset = offset
+                } else {
+                    val targetOffset = if (animateNode.lookaheadOffset != NotInitialized) {
+                        animateNode.lookaheadOffset
+                    } else {
+                        offset
+                    }
+                    val animatedOffset = targetOffset + animateNode.placementDelta
+                    // cancel the animation if current and target offsets are both out of the bounds
+                    if ((targetOffset.mainAxis <= minOffset &&
+                            animatedOffset.mainAxis <= minOffset) ||
+                        (targetOffset.mainAxis >= maxOffset &&
+                            animatedOffset.mainAxis >= maxOffset)
+                    ) {
+                        animateNode.cancelAnimation()
+                    }
+                    offset = animatedOffset
                 }
-                offset = animatedOffset
             }
             if (reverseLayout) {
                 offset = offset.copy { mainAxisOffset ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 1d2cf7a..fb2fe0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -16,6 +16,13 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.copy
+import androidx.compose.animation.core.spring
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.MutatePriority
 import androidx.compose.foundation.gestures.Orientation
@@ -42,7 +49,10 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
 import kotlin.math.abs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /**
  * Creates a [LazyListState] that is remembered across compositions.
@@ -82,6 +92,12 @@
     firstVisibleItemIndex: Int = 0,
     firstVisibleItemScrollOffset: Int = 0
 ) : ScrollableState {
+
+    internal var hasLookaheadPassOccurred: Boolean = false
+        private set
+    internal var postLookaheadLayoutInfo: LazyListLayoutInfo? = null
+        private set
+
     /**
      * The holder class for the current scroll position.
      */
@@ -385,18 +401,69 @@
     /**
      *  Updates the state with the new calculated scroll position and consumed scroll.
      */
-    internal fun applyMeasureResult(result: LazyListMeasureResult) {
-        scrollPosition.updateFromMeasureResult(result)
-        scrollToBeConsumed -= result.consumedScroll
-        layoutInfoState.value = result
+    internal fun applyMeasureResult(result: LazyListMeasureResult, isLookingAhead: Boolean) {
+        if (!isLookingAhead && hasLookaheadPassOccurred) {
+            // If there was already a lookahead pass, record this result as postLookahead result
+            postLookaheadLayoutInfo = result
+        } else {
+            if (isLookingAhead) {
+                hasLookaheadPassOccurred = true
+            }
+            scrollPosition.updateFromMeasureResult(result)
+            scrollToBeConsumed -= result.consumedScroll
+            layoutInfoState.value = result
 
-        canScrollForward = result.canScrollForward
-        canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
-            result.firstVisibleItemScrollOffset != 0
+            canScrollForward = result.canScrollForward
+            canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
+                result.firstVisibleItemScrollOffset != 0
 
-        numMeasurePasses++
+            if (isLookingAhead) updateScrollDeltaForPostLookahead(result.scrollBackAmount)
+            numMeasurePasses++
 
-        cancelPrefetchIfVisibleItemsChanged(result)
+            cancelPrefetchIfVisibleItemsChanged(result)
+        }
+    }
+
+    internal var coroutineScope: CoroutineScope? = null
+
+    internal val scrollDeltaBetweenPasses: Float
+        get() = _scrollDeltaBetweenPasses.value
+
+    private var _scrollDeltaBetweenPasses: AnimationState<Float, AnimationVector1D> =
+        AnimationState(Float.VectorConverter, 0f, 0f)
+
+    // Updates the scroll delta between lookahead & post-lookahead pass
+    private fun updateScrollDeltaForPostLookahead(delta: Float) {
+        if (delta <= with(density) { DeltaThresholdForScrollAnimation.toPx() }) {
+            // If the delta is within the threshold, scroll by the delta amount instead of animating
+            return
+        }
+
+        // Scroll delta is updated during lookahead, we don't need to trigger lookahead when
+        // the delta changes.
+        Snapshot.withoutReadObservation {
+            val currentDelta = _scrollDeltaBetweenPasses.value
+
+            if (_scrollDeltaBetweenPasses.isRunning) {
+                _scrollDeltaBetweenPasses = _scrollDeltaBetweenPasses.copy(currentDelta - delta)
+                coroutineScope?.launch {
+                    _scrollDeltaBetweenPasses.animateTo(
+                        0f,
+                        spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 0.5f),
+                        true
+                    )
+                }
+            } else {
+                _scrollDeltaBetweenPasses = AnimationState(Float.VectorConverter, -delta)
+                coroutineScope?.launch {
+                    _scrollDeltaBetweenPasses.animateTo(
+                        0f,
+                        spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 0.5f),
+                        true
+                    )
+                }
+            }
+        }
     }
 
     /**
@@ -425,6 +492,8 @@
     }
 }
 
+private val DeltaThresholdForScrollAnimation = 1.dp
+
 private object EmptyLazyListLayoutInfo : LazyListLayoutInfo {
     override val visibleItemsInfo = emptyList<LazyListItemInfo>()
     override val viewportStartOffset = 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateItemModifierNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateItemModifierNode.kt
index dc2161f..8a81ca6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateItemModifierNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateItemModifierNode.kt
@@ -74,6 +74,12 @@
     }
 
     /**
+     * Tracks the offset of the item in the lookahead pass. When set, this is the animation target
+     * that placementDelta should be applied to.
+     */
+    var lookaheadOffset: IntOffset = NotInitialized
+
+    /**
      * Animate the placement by the given [delta] offset.
      */
     fun animatePlacementDelta(delta: IntOffset) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 51ef3b9..0ff47af 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -1003,11 +1003,7 @@
     private val measureScope: LazyLayoutMeasureScope,
     private val resolvedSlots: LazyStaggeredGridSlots
 ) {
-    private fun childConstraints(requestedSlot: Int, requestedSpan: Int): Constraints {
-        val slotCount = resolvedSlots.sizes.size
-        val slot = requestedSlot.coerceAtMost(slotCount - 1)
-        val span = requestedSpan.coerceAtMost(slotCount - slot)
-
+    private fun childConstraints(slot: Int, span: Int): Constraints {
         // resolved slots contain [offset, size] pair per each slot.
         val crossAxisSize = if (span == 1) {
             resolvedSlots.sizes[slot]
@@ -1028,11 +1024,16 @@
     fun getAndMeasure(index: Int, span: SpanRange): LazyStaggeredGridMeasuredItem {
         val key = itemProvider.getKey(index)
         val contentType = itemProvider.getContentType(index)
-        val placeables = measureScope.measure(index, childConstraints(span.start, span.size))
+
+        val slotCount = resolvedSlots.sizes.size
+        val spanStart = span.start.coerceAtMost(slotCount - 1)
+        val spanSize = span.size.coerceAtMost(slotCount - spanStart)
+
+        val placeables = measureScope.measure(index, childConstraints(spanStart, spanSize))
         return createItem(
             index,
-            span.start,
-            span.size,
+            spanStart,
+            spanSize,
             key,
             contentType,
             placeables
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index f179e9e..85527e5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -814,7 +814,7 @@
         }
 
         if (!state.is24hour) {
-            Box(modifier.padding(start = PeriodToggleMargin)) {
+            Box(Modifier.padding(start = PeriodToggleMargin)) {
                 VerticalPeriodToggle(
                     modifier = Modifier.size(
                         PeriodSelectorContainerWidth,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index a1e73ac..6e3afac 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -62,6 +62,7 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Matrix
@@ -2145,6 +2146,85 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun lookaheadSizeTrackedWhenModifierChanges() {
+        var expanded by mutableStateOf(true)
+        val lookaheadHeight = mutableListOf(0, 0, 0)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    Layout(content = {
+                        repeat(3) {
+                            Column(Modifier.layout { measurable, constraints ->
+                                measurable.measure(constraints).run {
+                                    layout(width, height) { place(0, 0) }
+                                }
+                            }) {
+                                Box(
+                                    Modifier
+                                        .requiredHeight(100.dp)
+                                        .fillMaxWidth()
+                                ) {
+                                    Text("$it")
+                                }
+                                // Bring in a new modifier while setting the size to 0.
+                                Box(
+                                    (if (!expanded) Modifier.clipToBounds() else Modifier)
+                                        .then(Modifier.layout { measurable, constraints ->
+                                            measurable.measure(constraints).run {
+                                                val (w, h) = if (isLookingAhead) {
+                                                    if (!expanded) IntSize.Zero else IntSize(
+                                                        width,
+                                                        height
+                                                    )
+                                                } else {
+                                                    IntSize(width, height)
+                                                }
+                                                layout(w, h) {
+                                                    place(0, 0)
+                                                }
+                                            }
+                                        })
+                                ) {
+                                    Box(
+                                        Modifier
+                                            .requiredHeight(100.dp)
+                                            .fillMaxWidth()
+                                    )
+                                }
+                            }
+                        }
+                    }) { measurables, constraints ->
+                        measurables.map { it.measure(constraints) }.run {
+                            layout(this[0].width, this[0].height * 3) {
+                                var h = 0
+                                forEachIndexed { id, placeable ->
+                                    if (isLookingAhead) {
+                                        lookaheadHeight[id] = placeable.height
+                                    }
+                                    placeable.place(0, h)
+                                    h += placeable.height
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        repeat(3) {
+            assertEquals(200, lookaheadHeight[it])
+        }
+        rule.runOnIdle {
+            expanded = false
+        }
+        rule.waitForIdle()
+        repeat(3) {
+            assertEquals(100, lookaheadHeight[it])
+        }
+    }
+
     @Test
     fun forceMeasureSubtreeWhileLookaheadMeasureRequestedFromSubtree() {
         var iterations by mutableStateOf(0)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 263068d..7d9003a3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.ReusableContentHost
 import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.currentCompositeKeyHash
 import androidx.compose.runtime.getValue
@@ -48,6 +49,7 @@
 import androidx.compose.ui.platform.createSubcomposition
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
 
 /**
  * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
@@ -358,7 +360,6 @@
  * when a new SubcomposeLayoutState is applied to SubcomposeLayout and even when the
  * SubcomposeLayout's LayoutNode is reused via the ReusableComposeNode mechanism.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 internal class LayoutNodeSubcompositionsState(
     private val root: LayoutNode,
     slotReusePolicy: SubcomposeSlotReusePolicy
@@ -374,10 +375,8 @@
             }
         }
 
-    val isInLookaheadScope: Boolean
-        get() = root.lookaheadRoot != null
-
     private var currentIndex = 0
+    private var currentPostLookaheadIndex = 0
     private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState>()
 
     // this map contains active slotIds (without precomposed or reusable nodes)
@@ -387,6 +386,11 @@
 
     private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
     private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
+    // SlotHandles precomposed in the post-lookahead pass.
+    private val postLookaheadPrecomposeSlotHandleMap = mutableMapOf<Any?, PrecomposedSlotHandle>()
+    // Slot ids _composed_ in post-lookahead. The valid slot ids are stored between 0 and
+    // currentPostLookaheadIndex - 1, beyond index currentPostLookaheadIndex are obsolete ids.
+    private val postLookaheadComposedSlotIds = mutableVectorOf<Any?>()
 
     /**
      * `root.foldedChildren` list consist of:
@@ -622,32 +626,51 @@
                 scope.density = density
                 scope.fontScale = fontScale
                 if (!isLookingAhead && root.lookaheadRoot != null) {
-                    return with(postLookaheadMeasureScope) {
-                        block(constraints)
+                    currentPostLookaheadIndex = 0
+                    val result = postLookaheadMeasureScope.block(constraints)
+                    val indexAfterMeasure = currentPostLookaheadIndex
+                    return createMeasureResult(result) {
+                        currentPostLookaheadIndex = indexAfterMeasure
+                        result.placeChildren()
+                        // dispose
+                        disposeUnusedSlotsInPostLookahead()
                     }
                 } else {
                     currentIndex = 0
                     val result = scope.block(constraints)
                     val indexAfterMeasure = currentIndex
-                    return object : MeasureResult {
-                        override val width: Int
-                            get() = result.width
-                        override val height: Int
-                            get() = result.height
-                        override val alignmentLines: Map<AlignmentLine, Int>
-                            get() = result.alignmentLines
-
-                        override fun placeChildren() {
-                            currentIndex = indexAfterMeasure
-                            result.placeChildren()
-                            disposeOrReuseStartingFromIndex(currentIndex)
-                        }
+                    return createMeasureResult(result) {
+                        currentIndex = indexAfterMeasure
+                        result.placeChildren()
+                        disposeOrReuseStartingFromIndex(currentIndex)
                     }
                 }
             }
         }
     }
 
+    private fun disposeUnusedSlotsInPostLookahead() {
+        postLookaheadPrecomposeSlotHandleMap.entries.removeAll { (slotId, handle) ->
+            val id = postLookaheadComposedSlotIds.indexOf(slotId)
+            if (id < 0 || id >= currentPostLookaheadIndex) {
+                // Slot was not used in the latest pass of post-lookahead.
+                handle.dispose()
+                true
+            } else {
+                false
+            }
+        }
+    }
+
+    private inline fun createMeasureResult(
+        result: MeasureResult,
+        crossinline placeChildrenBlock: () -> Unit
+    ) = object : MeasureResult by result {
+        override fun placeChildren() {
+            placeChildrenBlock()
+        }
+    }
+
     private val NoIntrinsicsMessage = "Asking for intrinsic measurements of SubcomposeLayout " +
         "layouts is not supported. This includes components that are built on top of " +
         "SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate " +
@@ -661,6 +684,8 @@
     fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle {
         makeSureStateIsConsistent()
         if (!slotIdToNode.containsKey(slotId)) {
+            // Yield ownership of PrecomposedHandle from postLookahead to the caller of precompose
+            postLookaheadPrecomposeSlotHandleMap.remove(slotId)
             val node = precomposeMap.getOrPut(slotId) {
                 val reusedNode = takeNodeFromReusables(slotId)
                 if (reusedNode != null) {
@@ -785,8 +810,46 @@
          * the subcomposition that happened in the lookahead pass. If [slotId] was not subcomposed
          * in the lookahead pass, [subcompose] will return an [emptyList].
          */
-        override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> =
-            slotIdToNode[slotId]?.childMeasurables ?: emptyList()
+        override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
+            val measurables = slotIdToNode[slotId]?.childMeasurables
+            if (measurables != null) {
+                return measurables
+            }
+            return postLookaheadSubcompose(slotId, content)
+        }
+    }
+
+    private fun postLookaheadSubcompose(
+        slotId: Any?,
+        content: @Composable () -> Unit
+    ): List<Measurable> {
+        require(postLookaheadComposedSlotIds.size >= currentPostLookaheadIndex) {
+            "Error: currentPostLookaheadIndex cannot be greater than the size of the" +
+                "postLookaheadComposedSlotIds list."
+        }
+        if (postLookaheadComposedSlotIds.size == currentPostLookaheadIndex) {
+            postLookaheadComposedSlotIds.add(slotId)
+        } else {
+            postLookaheadComposedSlotIds[currentPostLookaheadIndex] = slotId
+        }
+        currentPostLookaheadIndex++
+        if (!precomposeMap.contains(slotId)) {
+            // Not composed yet
+            precompose(slotId, content).also {
+                postLookaheadPrecomposeSlotHandleMap[slotId] = it
+            }
+            if (root.layoutState == LayoutState.LayingOut) {
+                root.requestLookaheadRelayout(true)
+            } else {
+                root.requestLookaheadRemeasure(true)
+            }
+        }
+
+        return precomposeMap[slotId]?.run {
+            measurePassDelegate.childDelegates.also {
+                it.fastForEach { delegate -> delegate.markDetachedFromParentLookaheadPass() }
+            }
+        } ?: emptyList()
     }
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index d58eef5..75aaace 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -47,6 +47,13 @@
         get() = measurePassDelegate.width
 
     /**
+     * This gets set to true via [MeasurePassDelegate.markDetachedFromParentLookaheadPass] and
+     * automatically gets unset in `measure` when the measure call comes from parent with
+     * layoutState being LookaheadMeasuring or LookaheadLayingOut.
+     */
+    private var detachedFromParentLookaheadPass: Boolean = false
+
+    /**
      * The layout state the node is currently in.
      *
      * The mutation of [layoutState] is confined to [LayoutNodeLayoutDelegate], and is therefore
@@ -332,6 +339,10 @@
                 return _childDelegates.asMutableList()
             }
 
+        internal fun markDetachedFromParentLookaheadPass() {
+            detachedFromParentLookaheadPass = true
+        }
+
         var layingOutChildren = false
             private set
 
@@ -1135,6 +1146,10 @@
         }
 
         override fun measure(constraints: Constraints): Placeable {
+            if (layoutNode.parent?.layoutState == LayoutState.LookaheadMeasuring ||
+                layoutNode.parent?.layoutState == LayoutState.LookaheadLayingOut) {
+                detachedFromParentLookaheadPass = false
+            }
             trackLookaheadMeasurementByParent(layoutNode)
             if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {
                 // This LayoutNode may have asked children for intrinsics. If so, we should
@@ -1191,14 +1206,19 @@
                 forEachChildAlignmentLinesOwner {
                     it.alignmentLines.usedDuringParentMeasurement = false
                 }
+                // Copy out the previous size before performing lookahead measure. If never
+                // measured, set the last size to negative instead of Zero in anticipation for zero
+                // being a valid lookahead size.
+                val lastLookaheadSize = if (measuredOnce)
+                    measuredSize
+                else
+                    IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
                 measuredOnce = true
                 val lookaheadDelegate = outerCoordinator.lookaheadDelegate
                 check(lookaheadDelegate != null) {
                     "Lookahead result from lookaheadRemeasure cannot be null"
                 }
 
-                // Copy out the previous size before perform lookahead measure
-                val lastLookaheadSize = IntSize(lookaheadDelegate.width, lookaheadDelegate.height)
                 performLookaheadMeasure(constraints)
                 measuredSize = IntSize(lookaheadDelegate.width, lookaheadDelegate.height)
                 val sizeChanged = lastLookaheadSize.width != lookaheadDelegate.width ||
@@ -1482,7 +1502,7 @@
      * has a lookahead root.
      */
     private fun LayoutNode.isOutMostLookaheadRoot(): Boolean =
-        lookaheadRoot != null && parent?.lookaheadRoot == null
+        lookaheadRoot != null && (parent?.lookaheadRoot == null || detachedFromParentLookaheadPass)
 
     /**
      * Performs measure with the given constraints and perform necessary state mutations before
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
index 4ebfe68..4e2ee0f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
@@ -65,6 +65,7 @@
             // remeasure or relayout is scheduled
             if (measurePending) {
                 return relayoutNodes.contains(this) ||
+                    layoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
                     parent?.measurePending == true ||
                     parent?.lookaheadMeasurePending == true ||
                     parentLayoutState == LayoutNode.LayoutState.Measuring
diff --git a/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt b/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
index eaf2d0a..a7ecb4f 100644
--- a/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
+++ b/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
@@ -77,7 +77,7 @@
 
             // Add additional dependencies required for KSP outputs
 
-            val toolsVersion = "1.0.0-alpha03"
+            val toolsVersion = "1.0.0-alpha04"
             project.dependencies {
                 add(
                     "ksp",
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/AssetLoaderAjaxActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/AssetLoaderAjaxActivityTestAppTest.java
index 7325510..a84374d 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/AssetLoaderAjaxActivityTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/AssetLoaderAjaxActivityTestAppTest.java
@@ -20,10 +20,10 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,10 +52,10 @@
                 activity.getUriIdlingResource()));
     }
 
-    @Ignore("b/283485965")
     @Test
     public void testAssetLoaderAjaxActivity() {
-        mRule.getScenario().onActivity(activity -> activity.loadUrl());
+        mRule.getScenario().onActivity(AssetLoaderAjaxActivity::loadUrl);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
         WebkitTestHelpers.assertHtmlElementContainsText(R.id.webview_asset_loader_webview,
                 "title", "Loaded HTML should appear below on success");
         WebkitTestHelpers.assertHtmlElementContainsText(R.id.webview_asset_loader_webview,