[go: nahoru, domu]

Merge "Bump Fragment libraries to lifecycle 2.5.1 prebuilt" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index 3e7fefd..0cf6486 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -90,7 +90,7 @@
     }
 
     @Test
-    fun noActivityAvailableTest() {
+    fun noActivityAvailableLifecycleTest() {
         ActivityScenario.launch(RegisterInInitActivity::class.java).use { scenario ->
             var exceptionThrown = false
             scenario.withActivity {
@@ -107,6 +107,25 @@
             }
         }
     }
+
+    @Test
+    fun noActivityAvailableNoLifecycleTest() {
+        ActivityScenario.launch(RegisterInInitActivity::class.java).use { scenario ->
+            var exceptionThrown = false
+            scenario.withActivity {
+                try {
+                    launcherNoLifecycle.launch(Intent("no action"))
+                } catch (e: ActivityNotFoundException) {
+                    exceptionThrown = true
+                }
+            }
+
+            scenario.withActivity {
+                assertThat(exceptionThrown).isTrue()
+                assertThat(launchCount).isEqualTo(0)
+            }
+        }
+    }
 }
 
 class PassThroughActivity : ComponentActivity() {
@@ -175,12 +194,16 @@
 
 class RegisterInInitActivity : ComponentActivity() {
     var launcher: ActivityResultLauncher<Intent>
+    val launcherNoLifecycle: ActivityResultLauncher<Intent>
     var launchCount = 0
 
     init {
         launcher = registerForActivityResult(StartActivityForResult()) {
             launchCount++
         }
+        launcherNoLifecycle = activityResultRegistry.register("test", StartActivityForResult()) {
+            launchCount++
+        }
     }
 }
 
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
index 1a15c7e..7333e3e 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -242,7 +242,12 @@
                             + "before calling launch().");
                 }
                 mLaunchedKeys.add(key);
-                onLaunch(innerCode, contract, input, options);
+                try {
+                    onLaunch(innerCode, contract, input, options);
+                } catch (Exception e) {
+                    mLaunchedKeys.remove(key);
+                    throw e;
+                }
             }
 
             @Override
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 30c2a30..ff35c32 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -25,22 +25,10 @@
     id("kotlin")
 }
 
-apply from: "../buildSrc/kotlin-dsl-dependency.gradle"
-
-def buildSrcJar(jarName) {
-    return project.files(
-            new File(
-                    BuildServerConfigurationKt.getRootOutDirectory(project),
-                    "buildSrc/$jarName/build/libs/${jarName}.jar"
-            )
-    )
-}
-
 dependencies {
     implementation(gradleApi())
-    implementation(buildSrcJar("private"))
-    implementation(buildSrcJar("public"))
-    implementation(buildSrcJar("jetpad-integration"))
+    implementation(project.files(new File(BuildServerConfigurationKt.getRootOutDirectory(project), "buildSrc/private/build/libs/private.jar")))
+    implementation(project.files(new File(BuildServerConfigurationKt.getRootOutDirectory(project), "buildSrc/public/build/libs/public.jar")))
     implementation("com.googlecode.json-simple:json-simple:1.1")
     implementation(libs.gson)
     implementation(libs.dom4j) {
@@ -55,9 +43,6 @@
     testImplementation(project(":internal-testutils-gradle-plugin"))
     testImplementation(gradleTestKit())
     testImplementation(libs.checkmark)
-    testImplementation(libs.kotlinGradlePluginz)
-    testImplementation(libs.toml)
-    testImplementation(findGradleKotlinDsl())
 }
 
 SdkResourceGenerator.generateForHostTest(project)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
index 00b98c0..5e08ae5 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
@@ -55,9 +55,6 @@
  * @param setup: Gradle project setup (see [ProjectSetupRule])
  */
 data class AndroidXPluginTestContext(val tmpFolder: TemporaryFolder, val setup: ProjectSetupRule) {
-    // Default empty environment for runGradle (otherwise the host environment leaks through)
-    private val defaultEnv: Map<String, String> = mapOf()
-
     val props = setup.props
     val buildJars = BuildJars(props.buildSrcOutPath)
 
@@ -67,9 +64,9 @@
     // Gradle sometimes canonicalizes this path, so we have to or things don't match up.
     val supportRoot: File = setup.rootDir.canonicalFile
 
-    fun runGradle(vararg args: String) = runGradleWithEnv(defaultEnv, *args)
-
-    fun runGradleWithEnv(env: Map<String, String>, vararg args: String): BuildResult {
+    fun runGradle(vararg args: String): BuildResult {
+        // Empty environment so that the host environment does not leak through
+        val env = mapOf<String, String>()
         return GradleRunner.create().withProjectDir(supportRoot)
             .withArguments(
                 "-Dmaven.repo.local=$mavenLocalDir",
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
index 53cb07a..bb8f4eb 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXRootPluginTest.kt
@@ -17,7 +17,6 @@
 package androidx.build
 
 import androidx.build.AndroidXPluginTestContext.Companion.fileList
-import androidx.build.AndroidXSelfTestProject.Companion.buildGradleForKmp
 import androidx.build.AndroidXSelfTestProject.Companion.cubaneKmpProject
 import androidx.build.AndroidXSelfTestProject.Companion.cubaneProject
 import net.saff.checkmark.Checkmark.Companion.check
@@ -85,50 +84,6 @@
     }
 
     @Test
-    fun kmpBuildInfoTasks() = pluginTest {
-        val gitChangeFilesDir = tmpFolder.newFolder()
-        val gitChangeInfoFilename = gitChangeFilesDir.resolve("CHANGE_INFO").apply {
-            writeText("{}")
-        }
-        val gitManifestFilename = gitChangeFilesDir.resolve("MANIFEST").apply {
-            writeText("path=\"frameworks/support\" revision=\"testRev\" ")
-        }
-        val env = mapOf(
-            "CHANGE_INFO" to gitChangeInfoFilename.path,
-            "MANIFEST" to gitManifestFilename.path
-        )
-
-        writeBuildFiles(
-            cubaneKmpProject.copy(
-                buildGradleText = buildGradleForKmp(
-                    withJava = true,
-                    addJvmDependency = true
-                )
-            )
-        )
-
-        // Make sure this exists and passes
-        runGradleWithEnv(
-            env,
-            ":cubane:cubanekmp:createLibraryBuildInfoFiles",
-            "--dry-run",
-            "--stacktrace"
-        )
-
-        // Run to generate build_info file to examine.
-        runGradleWithEnv(env, ":cubane:cubanekmp:createLibraryBuildInfoFilesJvm", "--stacktrace")
-
-        // Generated by command above.  See CreateLibraryBuildInfoFileTask doc for derivation
-        // of filename
-        val buildInfoPath = "dist/build-info/cubane_cubanekmp-jvm_build_info.txt"
-        outDir.resolve(buildInfoPath).readText().check {
-            it.contains("\"artifactId\": \"cubanekmp-jvm\"")
-        }.check {
-            it.contains("jvmdep")
-        }
-    }
-
-    @Test
     fun testSaveMavenFoldersForDebugging() = pluginTest {
         setupDocsProjects(cubaneProject)
         cubaneProject.publishMavenLocal()
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTest.kt
deleted file mode 100644
index 947d75c..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTest.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import androidx.build.LibraryBuildInfoTestContext.Companion.buildInfoTest
-import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask
-import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK
-import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask
-import androidx.build.buildInfo.ProjectPublishPlan
-import androidx.build.buildInfo.ProjectPublishPlan.VariantPublishPlan
-import androidx.build.buildInfo.addCreateLibraryBuildInfoFileTasksAfterEvaluate
-import net.saff.checkmark.Checkmark.Companion.check
-import org.gradle.api.Project
-import org.gradle.api.component.AdhocComponentWithVariants
-import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
-import org.gradle.api.internal.provider.DefaultProvider
-import org.gradle.api.tasks.TaskProvider
-import org.junit.Test
-
-class LibraryBuildInfoTest {
-    private val stubShaProvider = DefaultProvider { "stubSha" }
-
-    @Test
-    fun addTaskForSimpleSubProjectAndAddToAggregate() = buildInfoTest {
-        val aggregateTask = createAggregateBuildInfoTask()
-
-        with(AndroidXImplPlugin(stubComponentFactory())) {
-            val info = myGroupReleaseInfo(subProject)
-            subProject.addCreateLibraryBuildInfoFileTasksAfterEvaluate(info, stubShaProvider)
-        }
-
-        aggregateTask.dependencyNames().check { it.contains("createLibraryBuildInfoFiles") }
-    }
-
-    @Test
-    fun addTaskForCompoundSubProjectAndAddToAggregate() = buildInfoTest {
-        val aggregateTask = createAggregateBuildInfoTask()
-
-        subProject.configurations.register("someOtherConfiguration") { config ->
-            config.dependencies.add(
-                DefaultExternalModuleDependency(
-                    "androidx.specialGroup",
-                    "specialArtifact",
-                    "4.5.6"
-                )
-            )
-        }
-
-        with(AndroidXImplPlugin(stubComponentFactory())) {
-            subProject.addCreateLibraryBuildInfoFileTasksAfterEvaluate(
-                ProjectPublishPlan(
-                    shouldRelease = true,
-                    mavenGroup = LibraryGroup(group = "myGroup", atomicGroupVersion = null),
-                    variants = listOf(
-                        subProject.stubVariantPublishPlan,
-                        VariantPublishPlan(
-                            artifactId = "artifact-jvm",
-                            taskSuffix = "Jvm",
-                            configurationName = "someOtherConfiguration"
-                        )
-                    )
-                ), stubShaProvider
-            )
-        }
-
-        aggregateTask.dependencyNames().check {
-            it.containsAll(
-                listOf(
-                    "createLibraryBuildInfoFiles", "createLibraryBuildInfoFilesJvm"
-                )
-            )
-        }
-
-        getBuildInfoTask(name = "createLibraryBuildInfoFiles").let { task ->
-            task.artifactId.get().check { it == "subproject" }
-            task.dependencyList.get().check { it.isEmpty() }
-        }
-
-        getBuildInfoTask(name = "createLibraryBuildInfoFilesJvm").let { task ->
-            task.artifactId.get().check { it == "artifact-jvm" }
-            task.dependencyList.get().single().let { dep ->
-                dep.check { it.groupId == "androidx.specialGroup" }
-                dep.check { it.artifactId == "specialArtifact" }
-                dep.check { it.version == "4.5.6" }
-            }
-        }
-    }
-
-    private fun LibraryBuildInfoTestContext.getBuildInfoTask(name: String) =
-        subProject.tasks.findByName(name) as CreateLibraryBuildInfoFileTask
-
-    private fun TaskProvider<*>.dependencyNames() =
-        get().dependsOn.map { it as TaskProvider<*> }.map { it.get().name }
-
-    private fun myGroupReleaseInfo(project: Project) = ProjectPublishPlan(
-        shouldRelease = true,
-        mavenGroup = LibraryGroup(group = "myGroup", atomicGroupVersion = null),
-        variants = listOf(project.stubVariantPublishPlan)
-    )
-
-    private val Project.stubVariantPublishPlan: VariantPublishPlan
-        get() = VariantPublishPlan(artifactId = name)
-
-    private fun stubComponentFactory(): (String) -> AdhocComponentWithVariants =
-        { TODO("Should never be called in these tests") }
-
-    private fun LibraryBuildInfoTestContext.createAggregateBuildInfoTask() =
-        subProject.tasks.register(
-            CREATE_AGGREGATE_BUILD_INFO_FILES_TASK,
-            CreateAggregateLibraryBuildInfoFileTask::class.java
-        )
-}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTestContext.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTestContext.kt
deleted file mode 100644
index 4fe5382..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryBuildInfoTestContext.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import androidx.build.AndroidXPluginTestContext.Companion.wrap
-import org.gradle.api.Project
-import org.gradle.api.plugins.ExtraPropertiesExtension
-import org.gradle.testfixtures.ProjectBuilder
-import org.junit.rules.TemporaryFolder
-
-class LibraryBuildInfoTestContext(tmpFolder: TemporaryFolder) {
-    val project = createRootProject(tmpFolder)
-    val subProject: Project = addSubproject(project, tmpFolder)
-
-    private fun createRootProject(tmpFolder: TemporaryFolder): Project {
-        val project = ProjectBuilder.builder().build()!!
-        project.setFolders(tmpFolder)
-        return project
-    }
-
-    private fun addSubproject(project: Project, tmpFolder: TemporaryFolder): Project =
-        ProjectBuilder.builder().withName("subproject").build().also {
-            project.childProjects["subproject"] = it
-            it.setFolders(tmpFolder)
-        }
-
-    private fun Project.setFolders(tmpFolder: TemporaryFolder) {
-        setSupportRootFolder(tmpFolder.root)
-
-        val extension = rootProject.property("ext") as ExtraPropertiesExtension
-        val outDir = tmpFolder.newFolder()
-        extension.set("outDir", outDir)
-    }
-
-    companion object {
-        fun buildInfoTest(action: LibraryBuildInfoTestContext.() -> Unit) =
-            TemporaryFolder().wrap { LibraryBuildInfoTestContext(it).action() }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/ProjectPublishPlanTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/ProjectPublishPlanTest.kt
deleted file mode 100644
index 5c71b93..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/ProjectPublishPlanTest.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import androidx.build.LibraryBuildInfoTestContext.Companion.buildInfoTest
-import androidx.build.buildInfo.computePublishPlan
-import java.io.File
-import java.util.Date
-import net.saff.checkmark.Checkmark.Companion.check
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.artifacts.PublishArtifact
-import org.gradle.api.attributes.Bundling
-import org.gradle.api.attributes.Category
-import org.gradle.api.attributes.Usage
-import org.gradle.api.tasks.TaskDependency
-import org.gradle.kotlin.dsl.named
-import org.junit.Test
-
-class ProjectPublishPlanTest {
-    @Test
-    fun noReleaseWithoutPublish() = buildInfoTest {
-        val extension = createAndroidXExtension()
-        extension.computePublishPlan().check { !it.shouldRelease }
-    }
-
-    @Test
-    fun yesRelease() = buildInfoTest {
-        val extension = createAndroidXExtension()
-        extension.publish = Publish.SNAPSHOT_AND_RELEASE
-        extension.computePublishPlan().check { it.shouldRelease }
-    }
-
-    @Test
-    fun avoidShadowConfigs() = buildInfoTest {
-        val extension = createAndroidXExtension()
-        extension.publish = Publish.SNAPSHOT_AND_RELEASE
-        subProject.configurations.register("runtimeElements") {
-            setPublishableAttributes(it, "realArtifact")
-        }
-        subProject.configurations.register("shadowRuntimeElements") {
-            setPublishableAttributes(it, "shadowArtifact")
-            it.attributes.attribute(
-                Bundling.BUNDLING_ATTRIBUTE,
-                subProject.objects.named<Bundling>(Bundling.SHADOWED)
-            )
-        }
-        extension.computePublishPlan().variants.map { it.artifactId }
-            .check { it == listOf("realArtifact") }
-    }
-
-    private fun LibraryBuildInfoTestContext.setPublishableAttributes(
-        it: Configuration,
-        artifactName: String
-    ) {
-        it.attributes.attribute(
-            Usage.USAGE_ATTRIBUTE,
-            project.objects.named<Usage>(Usage.JAVA_RUNTIME)
-        )
-        it.attributes.attribute(
-            Category.CATEGORY_ATTRIBUTE,
-            project.objects.named<Category>(Category.LIBRARY)
-        )
-        it.artifacts.add(stubArtifact(artifactName))
-    }
-
-    private fun stubArtifact(artifactName: String) = object : PublishArtifact {
-        override fun getName(): String {
-            return artifactName
-        }
-
-        override fun getBuildDependencies(): TaskDependency {
-            TODO("Not yet implemented")
-        }
-
-        override fun getExtension(): String {
-            TODO("Not yet implemented")
-        }
-
-        override fun getType(): String {
-            TODO("Not yet implemented")
-        }
-
-        override fun getClassifier(): String? {
-            TODO("Not yet implemented")
-        }
-
-        override fun getFile(): File {
-            TODO("Not yet implemented")
-        }
-
-        override fun getDate(): Date? {
-            TODO("Not yet implemented")
-        }
-    }
-
-    private fun LibraryBuildInfoTestContext.createAndroidXExtension(): AndroidXExtension {
-        writeLibraryVersionsFile(subProject.getSupportRootFolder(), listOf())
-
-        return subProject.extensions.create(
-            AndroidXImplPlugin.EXTENSION_NAME,
-            AndroidXExtension::class.java,
-            subProject
-        )
-    }
-}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/SdkResourceGeneratorTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/SdkResourceGeneratorTest.kt
index a571f1a..c58d39f 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/SdkResourceGeneratorTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/SdkResourceGeneratorTest.kt
@@ -31,8 +31,8 @@
 
         val project = ProjectBuilder.builder().build()
 
-        project.setSupportRootFolder(File("files/support"))
         val extension = project.rootProject.property("ext") as ExtraPropertiesExtension
+        extension.set("supportRootFolder", File("files/support"))
         extension.set("buildSrcOut", project.projectDir.resolve("relative/path"))
 
         SdkResourceGenerator.registerSdkResourceGeneratorTask(project)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/buildFiles.kt b/buildSrc-tests/src/test/kotlin/androidx/build/buildFiles.kt
index 2a47b4e..f2adaf6 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/buildFiles.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/buildFiles.kt
@@ -22,47 +22,38 @@
     writeBuildFiles(projects.toList())
 }
 
-fun AndroidXPluginTestContext.writeBuildFiles(
-    projects: List<AndroidXSelfTestProject>,
-    groupLines: List<String> = listOf()
-) {
+fun AndroidXPluginTestContext.writeBuildFiles(projects: List<AndroidXSelfTestProject>) {
     writeRootSettingsFile(projects.map { it.gradlePath })
     writeRootBuildFile()
     writeApplyPluginScript()
 
-    writeLibraryVersionsFile(supportRoot, groupLines)
+    File(supportRoot, "libraryversions.toml").writeText(
+        """|[groups]
+               |[versions]
+               |""".trimMargin()
+    )
 
     // Matches behavior of root properties
     File(supportRoot, "gradle.properties").writeText(
         """|# Do not automatically include stdlib
-           |kotlin.stdlib.default.dependency=false
-           |
-           |# Avoid OOM in subgradle
-           |# (https://github.com/gradle/gradle/issues/10527#issuecomment-887704062)
-           |org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=1g
-           |""".trimMargin()
+               |kotlin.stdlib.default.dependency=false
+               |
+               |# Avoid OOM in subgradle
+               |# (https://github.com/gradle/gradle/issues/10527#issuecomment-887704062)
+               |org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=1g
+               |""".trimMargin()
     )
 
     projects.forEach { it.writeFiles() }
 }
 
-fun writeLibraryVersionsFile(supportFolder: File, groupLines: List<String>) {
-    File(supportFolder, "libraryversions.toml").writeText(
-        buildString {
-            appendLine("[groups]")
-            groupLines.forEach { appendLine(it) }
-            appendLine("[versions]")
-        }
-    )
-}
-
 fun AndroidXPluginTestContext.writeRootSettingsFile(projectPaths: List<String>) {
     val settingsString = buildString {
         append(
             """|pluginManagement {
-               |  ${setup.repositories}
-               |}
-               |""".trimMargin()
+                   |  ${setup.repositories}
+                   |}
+                   |""".trimMargin()
         )
         appendLine()
         projectPaths.forEach {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 20b6cc5..cd7042e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -25,7 +25,6 @@
 import androidx.build.SupportConfig.DEFAULT_MIN_SDK_VERSION
 import androidx.build.SupportConfig.INSTRUMENTATION_RUNNER
 import androidx.build.SupportConfig.TARGET_SDK_VERSION
-import androidx.build.buildInfo.addCreateLibraryBuildInfoFileTasks
 import androidx.build.checkapi.JavaApiTaskConfig
 import androidx.build.checkapi.KmpApiTaskConfig
 import androidx.build.checkapi.LibraryApiTaskConfig
@@ -54,19 +53,12 @@
 import com.android.build.gradle.TestedExtension
 import com.android.build.gradle.internal.tasks.AnalyticsRecordingTask
 import com.android.build.gradle.internal.tasks.ListingFileRedirectTask
-import java.io.File
-import java.time.Duration
-import java.util.Locale
-import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
 import org.gradle.api.GradleException
-import org.gradle.api.JavaVersion.VERSION_11
 import org.gradle.api.JavaVersion.VERSION_1_8
+import org.gradle.api.JavaVersion.VERSION_11
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.Task
-import org.gradle.api.artifacts.repositories.IvyArtifactRepository
-import org.gradle.api.component.SoftwareComponentFactory
 import org.gradle.api.file.DuplicatesStrategy
 import org.gradle.api.plugins.JavaPlugin
 import org.gradle.api.plugins.JavaPluginExtension
@@ -79,18 +71,25 @@
 import org.gradle.api.tasks.testing.Test
 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
 import org.gradle.api.tasks.testing.logging.TestLogEvent
-import org.gradle.kotlin.dsl.KotlinClosure1
 import org.gradle.kotlin.dsl.create
 import org.gradle.kotlin.dsl.extra
 import org.gradle.kotlin.dsl.findByType
 import org.gradle.kotlin.dsl.getByType
-import org.gradle.kotlin.dsl.register
 import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
-import org.jetbrains.kotlin.gradle.targets.native.KotlinNativeHostTestRun
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.io.File
+import java.time.Duration
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import org.gradle.api.component.SoftwareComponentFactory
+import org.gradle.api.artifacts.repositories.IvyArtifactRepository
+import org.gradle.kotlin.dsl.KotlinClosure1
+import org.gradle.kotlin.dsl.register
+import org.jetbrains.kotlin.gradle.targets.native.KotlinNativeHostTestRun
 import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
 import org.jetbrains.kotlin.gradle.testing.KotlinTaskTestRun
 
@@ -418,7 +417,7 @@
         project.configurePublicResourcesStub(libraryExtension)
         project.configureSourceJarForAndroid(libraryExtension)
         project.configureVersionFileWriter(libraryExtension, androidXExtension)
-        project.addCreateLibraryBuildInfoFileTasks(androidXExtension)
+        project.addCreateLibraryBuildInfoFileTask(androidXExtension)
         project.configureJavaCompilationWarnings(androidXExtension)
 
         project.configureDependencyVerification(androidXExtension) { taskProvider ->
@@ -480,7 +479,7 @@
             }
         }
 
-        project.addCreateLibraryBuildInfoFileTasks(extension)
+        project.addCreateLibraryBuildInfoFileTask(extension)
 
         // Standard lint, docs, and Metalava configuration for AndroidX projects.
         project.configureNonAndroidProjectForLint(extension)
@@ -834,10 +833,39 @@
         }
     }
 
+    // Task that creates a json file of a project's dependencies
+    private fun Project.addCreateLibraryBuildInfoFileTask(extension: AndroidXExtension) {
+        afterEvaluate {
+            if (extension.shouldRelease()) {
+                // Only generate build info files for published libraries.
+                val task = CreateLibraryBuildInfoFileTask.setup(project, extension)
+
+                rootProject.tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME).configure {
+                    it.dependsOn(task)
+                }
+                addTaskToAggregateBuildInfoFileTask(task)
+            }
+        }
+    }
+
+    private fun Project.addTaskToAggregateBuildInfoFileTask(
+        task: TaskProvider<CreateLibraryBuildInfoFileTask>
+    ) {
+        rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {
+            val aggregateLibraryBuildInfoFileTask: CreateAggregateLibraryBuildInfoFileTask = it
+                as CreateAggregateLibraryBuildInfoFileTask
+            aggregateLibraryBuildInfoFileTask.dependsOn(task)
+            aggregateLibraryBuildInfoFileTask.libraryBuildInfoFiles.add(
+                task.flatMap { task -> task.outputFile }
+            )
+        }
+    }
+
     companion object {
         const val BUILD_TEST_APKS_TASK = "buildTestApks"
         const val CHECK_RELEASE_READY_TASK = "checkReleaseReady"
         const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
+        const val CREATE_AGGREGATE_BUILD_INFO_FILES_TASK = "createAggregateBuildInfoFiles"
         const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
         const val REPORT_LIBRARY_METRICS_TASK = "reportLibraryMetrics"
         const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
index f4934f1..39af336 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
@@ -16,10 +16,7 @@
 
 package androidx.build
 
-import androidx.build.dependencyTracker.DependencyTracker
-import androidx.build.dependencyTracker.ProjectGraph
 import androidx.build.gradle.isRoot
-import androidx.build.playground.FindAffectedModulesTask
 import groovy.xml.DOMBuilder
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
@@ -63,14 +60,6 @@
         rootProject.subprojects {
             configureSubProject(it)
         }
-
-        rootProject.tasks.register(
-            "findAffectedModules",
-            FindAffectedModulesTask::class.java
-        ) { task ->
-            task.projectGraph = ProjectGraph(rootProject)
-            task.dependencyTracker = DependencyTracker(rootProject, task.logger)
-        }
     }
 
     private fun configureSubProject(project: Project) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index bae2c1b..fe5eb3d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -16,11 +16,8 @@
 
 package androidx.build
 
-import androidx.build.AndroidXImplPlugin.Companion.CREATE_LIBRARY_BUILD_INFO_FILES_TASK
 import androidx.build.AndroidXImplPlugin.Companion.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
 import androidx.build.AndroidXImplPlugin.Companion.ZIP_TEST_CONFIGS_WITH_APKS_TASK
-import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask
-import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK
 import androidx.build.dependencyTracker.AffectedModuleDetector
 import androidx.build.gradle.isRoot
 import androidx.build.license.CheckExternalDependencyLicensesTask
@@ -90,12 +87,12 @@
         buildOnServerTask.buildId = getBuildId()
         buildOnServerTask.dependsOn(
             tasks.register(
-                CREATE_AGGREGATE_BUILD_INFO_FILES_TASK,
+                AndroidXImplPlugin.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK,
                 CreateAggregateLibraryBuildInfoFileTask::class.java
             )
         )
         buildOnServerTask.dependsOn(
-            tasks.register(CREATE_LIBRARY_BUILD_INFO_FILES_TASK)
+            tasks.register(AndroidXImplPlugin.CREATE_LIBRARY_BUILD_INFO_FILES_TASK)
         )
 
         VerifyPlaygroundGradleConfigurationTask.createIfNecessary(project)?.let {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/CreateAggregateLibraryBuildInfoFileTask.kt
similarity index 81%
rename from buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt
rename to buildSrc/private/src/main/kotlin/androidx/build/CreateAggregateLibraryBuildInfoFileTask.kt
index dcf6620..69c5636 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/CreateAggregateLibraryBuildInfoFileTask.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2019 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.
@@ -14,21 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.build.buildInfo
+package androidx.build
 
-import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK
-import androidx.build.getDistributionDirectory
 import androidx.build.jetpad.LibraryBuildInfoFile
 import com.google.gson.Gson
-import java.io.File
 import org.gradle.api.DefaultTask
-import org.gradle.api.Project
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
 import org.gradle.work.DisableCachingByDefault
+import java.io.File
+import java.util.ArrayList
 
 /**
  * Task for a json file of all dependencies for each artifactId
@@ -107,20 +104,4 @@
             throw RuntimeException("JSON written to $outputFile was invalid.")
         }
     }
-
-    companion object {
-        const val CREATE_AGGREGATE_BUILD_INFO_FILES_TASK = "createAggregateBuildInfoFiles"
-    }
-}
-
-fun Project.addTaskToAggregateBuildInfoFileTask(
-    task: TaskProvider<CreateLibraryBuildInfoFileTask>
-) {
-    rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {
-        val aggregateLibraryBuildInfoFileTask = it as CreateAggregateLibraryBuildInfoFileTask
-        aggregateLibraryBuildInfoFileTask.dependsOn(task)
-        aggregateLibraryBuildInfoFileTask.libraryBuildInfoFiles.add(
-            task.flatMap { task -> task.outputFile }
-        )
-    }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
new file mode 100644
index 0000000..6f8ef25
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2019 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.build
+
+import androidx.build.gitclient.Commit
+import androidx.build.gitclient.GitClient
+import androidx.build.gitclient.GitCommitRange
+import androidx.build.jetpad.LibraryBuildInfoFile
+import com.google.gson.GsonBuilder
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ProjectDependency
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.work.DisableCachingByDefault
+import java.io.File
+import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
+
+/**
+ * This task generates a library build information file containing the artifactId, groupId, and
+ * version of public androidx dependencies and release checklist of the library for consumption
+ * by the Jetpack Release Service (JetPad).
+ */
+@DisableCachingByDefault(because = "uses git sha as input")
+abstract class CreateLibraryBuildInfoFileTask : DefaultTask() {
+    init {
+        group = "Help"
+        description = "Generates a file containing library build information serialized to json"
+    }
+
+    @get:OutputFile
+    abstract val outputFile: Property<File>
+
+    @get:Input
+    abstract val artifactId: Property<String>
+
+    @get:Input
+    abstract val groupId: Property<String>
+
+    @get:Input
+    abstract val version: Property<String>
+
+    @get:Optional
+    @get:Input
+    abstract val kotlinVersion: Property<String>
+
+    @get:Input
+    abstract val projectDir: Property<String>
+
+    @get:Input
+    abstract val commit: Property<String>
+
+    @get:Input
+    abstract val groupIdRequiresSameVersion: Property<Boolean>
+
+    @get:Input
+    abstract val groupZipPath: Property<String>
+
+    @get:Input
+    abstract val projectZipPath: Property<String>
+
+    @get:Input
+    val dependencyList: ListProperty<LibraryBuildInfoFile.Dependency> =
+        project.objects.listProperty(LibraryBuildInfoFile.Dependency::class.java)
+
+    /**
+     * the local project directory without the full framework/support root directory path
+     */
+    @get:Input
+    abstract val projectSpecificDirectory: Property<String>
+
+    private fun writeJsonToFile(info: LibraryBuildInfoFile) {
+        val resolvedOutputFile: File = outputFile.get()
+        val outputDir = resolvedOutputFile.parentFile
+        if (!outputDir.exists()) {
+            if (!outputDir.mkdirs()) {
+                throw RuntimeException(
+                    "Failed to create " +
+                        "output directory: $outputDir"
+                )
+            }
+        }
+        if (!resolvedOutputFile.exists()) {
+            if (!resolvedOutputFile.createNewFile()) {
+                throw RuntimeException(
+                    "Failed to create output dependency dump file: $outputFile"
+                )
+            }
+        }
+
+        // Create json object from the artifact instance
+        val gson = GsonBuilder().serializeNulls().setPrettyPrinting().create()
+        val serializedInfo: String = gson.toJson(info)
+        resolvedOutputFile.writeText(serializedInfo)
+    }
+
+    private fun resolveAndCollectDependencies(): LibraryBuildInfoFile {
+        val libraryBuildInfoFile = LibraryBuildInfoFile()
+        libraryBuildInfoFile.artifactId = artifactId.get()
+        libraryBuildInfoFile.groupId = groupId.get()
+        libraryBuildInfoFile.version = version.get()
+        libraryBuildInfoFile.path = projectDir.get()
+        libraryBuildInfoFile.sha = commit.get()
+        libraryBuildInfoFile.groupIdRequiresSameVersion = groupIdRequiresSameVersion.get()
+        libraryBuildInfoFile.groupZipPath = groupZipPath.get()
+        libraryBuildInfoFile.projectZipPath = projectZipPath.get()
+        libraryBuildInfoFile.kotlinVersion = kotlinVersion.orNull
+        libraryBuildInfoFile.checks = ArrayList()
+        libraryBuildInfoFile.dependencies = ArrayList(dependencyList.get())
+        return libraryBuildInfoFile
+    }
+
+    /**
+     * Task: createLibraryBuildInfoFile
+     * Iterates through each configuration of the project and builds the set of all dependencies.
+     * Then adds each dependency to the Artifact class as a project or prebuilt dependency.  Finally,
+     * writes these dependencies to a json file as a json object.
+     */
+    @TaskAction
+    fun createLibraryBuildInfoFile() {
+        val resolvedArtifact = resolveAndCollectDependencies()
+        writeJsonToFile(resolvedArtifact)
+    }
+
+    companion object {
+        const val TASK_NAME = "createLibraryBuildInfoFiles"
+
+        fun setup(project: Project, extension: AndroidXExtension):
+            TaskProvider<CreateLibraryBuildInfoFileTask> {
+                return project.tasks.register(
+                    TASK_NAME,
+                    CreateLibraryBuildInfoFileTask::class.java
+                ) { task ->
+                    val group = project.group.toString()
+                    val name = project.name.toString()
+                    task.outputFile.set(
+                        File(
+                            project.getBuildInfoDirectory(),
+                            "${group}_${name}_build_info.txt"
+                        )
+                    )
+                    task.artifactId.set(name)
+                    task.groupId.set(group)
+                    task.version.set(project.version.toString())
+                    task.kotlinVersion.set(project.getKotlinPluginVersion())
+                    task.projectDir.set(
+                        project.projectDir.absolutePath.removePrefix(
+                            project.getSupportRootFolder().absolutePath
+                        )
+                    )
+                    task.commit.set(
+                        project.provider {
+                            project.getFrameworksSupportCommitShaAtHead()
+                        }
+                    )
+                    task.groupIdRequiresSameVersion.set(extension.mavenGroup?.requireSameVersion)
+                    task.groupZipPath.set(project.getGroupZipPath())
+                    task.projectZipPath.set(project.getProjectZipPath())
+
+                    // Note:
+                    // `project.projectDir.toString().removePrefix(project.rootDir.toString())`
+                    // does not work because the project rootDir is not guaranteed to be a
+                    // substring of the projectDir
+                    task.projectSpecificDirectory.set(
+                        project.projectDir.absolutePath.removePrefix(
+                            project.getSupportRootFolder().absolutePath
+                        )
+                    )
+                    task.dependencyList.set(project.provider {
+                        val libraryDependencies = HashSet<LibraryBuildInfoFile.Dependency>()
+                        project.configurations.filter {
+                            it.name == "releaseRuntimeElements"
+                        }.forEach { configuration ->
+                            configuration.allDependencies.forEach { dep ->
+                                // Only consider androidx dependencies
+                                if (dep.group != null &&
+                                    dep.group.toString().startsWith("androidx.") &&
+                                    !dep.group.toString().startsWith("androidx.test")
+                                ) {
+                                    val androidXPublishedDependency =
+                                        LibraryBuildInfoFile.Dependency()
+                                    androidXPublishedDependency.artifactId = dep.name.toString()
+                                    androidXPublishedDependency.groupId = dep.group.toString()
+                                    androidXPublishedDependency.version = dep.version.toString()
+                                    androidXPublishedDependency.isTipOfTree =
+                                        dep is ProjectDependency
+                                    libraryDependencies.add(androidXPublishedDependency)
+                                }
+                            }
+                        }
+                        ArrayList(libraryDependencies).sortedWith(
+                            compareBy({ it.groupId }, { it.artifactId }, { it.version })
+                        )
+                    })
+                }
+            }
+
+        /* For androidx release notes, the most common use case is to track and publish the last sha
+         * of the build that is released.  Thus, we use frameworks/support to get the sha
+         */
+        private fun Project.getFrameworksSupportCommitShaAtHead(): String {
+            val gitClient = GitClient.create(
+                project.getSupportRootFolder(),
+                logger,
+                GitClient.getChangeInfoPath(project).get(),
+                GitClient.getManifestPath(project).get()
+            )
+            val commitList: List<Commit> =
+                gitClient
+                .getGitLog(
+                    GitCommitRange(
+                        fromExclusive = "",
+                        untilInclusive = "HEAD",
+                        n = 1
+                    ),
+                    keepMerges = true,
+                    fullProjectDir = getSupportRootFolder()
+                )
+            if (commitList.isEmpty()) {
+                throw RuntimeException("Failed to find git commit for HEAD!")
+            }
+            return commitList.first().sha
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
deleted file mode 100644
index dbecd45..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
- * Copyright (C) 2019 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.build.buildInfo
-
-import androidx.build.AndroidXExtension
-import androidx.build.LibraryGroup
-import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.getFrameworksSupportCommitShaAtHead
-import androidx.build.getBuildInfoDirectory
-import androidx.build.getGroupZipPath
-import androidx.build.getProjectZipPath
-import androidx.build.getSupportRootFolder
-import androidx.build.gitclient.Commit
-import androidx.build.gitclient.GitClient
-import androidx.build.gitclient.GitCommitRange
-import androidx.build.jetpad.LibraryBuildInfoFile
-import com.google.common.annotations.VisibleForTesting
-import com.google.gson.GsonBuilder
-import java.io.File
-import org.gradle.api.DefaultTask
-import org.gradle.api.Project
-import org.gradle.api.artifacts.ProjectDependency
-import org.gradle.api.provider.ListProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.work.DisableCachingByDefault
-import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
-
-/**
- * This task generates a library build information file containing the artifactId, groupId, and
- * version of public androidx dependencies and release checklist of the library for consumption
- * by the Jetpack Release Service (JetPad).
- *
- * Example:
- * If this task is configured
- * - for a project with group name "myGroup"
- * - on a variant with artifactId "myArtifact",
- * - and root project outDir is "out"
- * - and environment variable DIST_DIR is not set
- *
- * then the build info file will be written to
- * "out/dist/build-info/myGroup_myArtifact_build_info.txt"
- */
-@DisableCachingByDefault(because = "uses git sha as input")
-abstract class CreateLibraryBuildInfoFileTask : DefaultTask() {
-    init {
-        group = "Help"
-        description = "Generates a file containing library build information serialized to json"
-    }
-
-    @get:OutputFile
-    abstract val outputFile: Property<File>
-
-    @get:Input
-    abstract val artifactId: Property<String>
-
-    @get:Input
-    abstract val groupId: Property<String>
-
-    @get:Input
-    abstract val version: Property<String>
-
-    @get:Optional
-    @get:Input
-    abstract val kotlinVersion: Property<String>
-
-    @get:Input
-    abstract val projectDir: Property<String>
-
-    @get:Input
-    abstract val commit: Property<String>
-
-    @get:Input
-    abstract val groupIdRequiresSameVersion: Property<Boolean>
-
-    @get:Input
-    abstract val groupZipPath: Property<String>
-
-    @get:Input
-    abstract val projectZipPath: Property<String>
-
-    @get:Input
-    abstract val dependencyList: ListProperty<LibraryBuildInfoFile.Dependency>
-
-    /**
-     * the local project directory without the full framework/support root directory path
-     */
-    @get:Input
-    abstract val projectSpecificDirectory: Property<String>
-
-    private fun writeJsonToFile(info: LibraryBuildInfoFile) {
-        val resolvedOutputFile: File = outputFile.get()
-        val outputDir = resolvedOutputFile.parentFile
-        if (!outputDir.exists()) {
-            if (!outputDir.mkdirs()) {
-                throw RuntimeException(
-                    "Failed to create " +
-                        "output directory: $outputDir"
-                )
-            }
-        }
-        if (!resolvedOutputFile.exists()) {
-            if (!resolvedOutputFile.createNewFile()) {
-                throw RuntimeException(
-                    "Failed to create output dependency dump file: $outputFile"
-                )
-            }
-        }
-
-        // Create json object from the artifact instance
-        val gson = GsonBuilder().serializeNulls().setPrettyPrinting().create()
-        val serializedInfo: String = gson.toJson(info)
-        resolvedOutputFile.writeText(serializedInfo)
-    }
-
-    private fun resolveAndCollectDependencies(): LibraryBuildInfoFile {
-        val libraryBuildInfoFile = LibraryBuildInfoFile()
-        libraryBuildInfoFile.artifactId = artifactId.get()
-        libraryBuildInfoFile.groupId = groupId.get()
-        libraryBuildInfoFile.version = version.get()
-        libraryBuildInfoFile.path = projectDir.get()
-        libraryBuildInfoFile.sha = commit.get()
-        libraryBuildInfoFile.groupIdRequiresSameVersion = groupIdRequiresSameVersion.get()
-        libraryBuildInfoFile.groupZipPath = groupZipPath.get()
-        libraryBuildInfoFile.projectZipPath = projectZipPath.get()
-        libraryBuildInfoFile.kotlinVersion = kotlinVersion.orNull
-        libraryBuildInfoFile.checks = ArrayList()
-        libraryBuildInfoFile.dependencies = ArrayList(dependencyList.get())
-        return libraryBuildInfoFile
-    }
-
-    /**
-     * Task: createLibraryBuildInfoFile
-     * Iterates through each configuration of the project and builds the set of all dependencies.
-     * Then adds each dependency to the Artifact class as a project or prebuilt dependency.  Finally,
-     * writes these dependencies to a json file as a json object.
-     */
-    @TaskAction
-    fun createLibraryBuildInfoFile() {
-        val resolvedArtifact = resolveAndCollectDependencies()
-        writeJsonToFile(resolvedArtifact)
-    }
-
-    companion object {
-        const val TASK_NAME = "createLibraryBuildInfoFiles"
-
-        fun setup(
-            project: Project,
-            mavenGroup: LibraryGroup?,
-            variant: ProjectPublishPlan.VariantPublishPlan,
-            shaProvider: Provider<String>
-        ): TaskProvider<CreateLibraryBuildInfoFileTask> {
-            return project.tasks.register(
-                TASK_NAME + variant.taskSuffix.orEmpty(),
-                CreateLibraryBuildInfoFileTask::class.java
-            ) { task ->
-                val group = project.group.toString()
-                val artifactId = variant.artifactId
-                task.outputFile.set(
-                    File(
-                        project.getBuildInfoDirectory(),
-                        "${group}_${artifactId}_build_info.txt"
-                    )
-                )
-                task.artifactId.set(artifactId)
-                task.groupId.set(group)
-                task.version.set(project.version.toString())
-                task.kotlinVersion.set(project.getKotlinPluginVersion())
-                task.projectDir.set(
-                    project.projectDir.absolutePath.removePrefix(
-                        project.getSupportRootFolder().absolutePath
-                    )
-                )
-                task.commit.set(shaProvider)
-                task.groupIdRequiresSameVersion.set(mavenGroup?.requireSameVersion)
-                task.groupZipPath.set(project.getGroupZipPath())
-                task.projectZipPath.set(project.getProjectZipPath())
-
-                // Note:
-                // `project.projectDir.toString().removePrefix(project.rootDir.toString())`
-                // does not work because the project rootDir is not guaranteed to be a
-                // substring of the projectDir
-                task.projectSpecificDirectory.set(
-                    project.projectDir.absolutePath.removePrefix(
-                        project.getSupportRootFolder().absolutePath
-                    )
-                )
-                task.dependencyList.set(project.provider {
-                    val libraryDependencies = HashSet<LibraryBuildInfoFile.Dependency>()
-                    project.configurations.filter {
-                        it.name == variant.configurationName
-                    }.forEach { configuration ->
-                        configuration.allDependencies.forEach { dep ->
-                            // Only consider androidx dependencies
-                            if (dep.group != null &&
-                                dep.group.toString().startsWith("androidx.") &&
-                                !dep.group.toString().startsWith("androidx.test")
-                            ) {
-                                val androidXPublishedDependency =
-                                    LibraryBuildInfoFile.Dependency()
-                                androidXPublishedDependency.artifactId = dep.name.toString()
-                                androidXPublishedDependency.groupId = dep.group.toString()
-                                androidXPublishedDependency.version = dep.version.toString()
-                                androidXPublishedDependency.isTipOfTree =
-                                    dep is ProjectDependency
-                                libraryDependencies.add(androidXPublishedDependency)
-                            }
-                        }
-                    }
-                    ArrayList(libraryDependencies).sortedWith(
-                        compareBy({ it.groupId }, { it.artifactId }, { it.version })
-                    )
-                })
-            }
-        }
-
-        /* For androidx release notes, the most common use case is to track and publish the last sha
-         * of the build that is released.  Thus, we use frameworks/support to get the sha
-         */
-        fun Project.getFrameworksSupportCommitShaAtHead(): String {
-            val gitClient = GitClient.create(
-                project.getSupportRootFolder(),
-                logger,
-                GitClient.getChangeInfoPath(project).get(),
-                GitClient.getManifestPath(project).get()
-            )
-            val commitList: List<Commit> =
-                gitClient
-                    .getGitLog(
-                        GitCommitRange(
-                            fromExclusive = "",
-                            untilInclusive = "HEAD",
-                            n = 1
-                        ),
-                        keepMerges = true,
-                        fullProjectDir = getSupportRootFolder()
-                    )
-            if (commitList.isEmpty()) {
-                throw RuntimeException("Failed to find git commit for HEAD!")
-            }
-            return commitList.first().sha
-        }
-    }
-}
-
-// Tasks that create a json files of a project's variant's dependencies
-fun Project.addCreateLibraryBuildInfoFileTasks(extension: AndroidXExtension) {
-    // It would be good to not have to split this up as it is, but moving forward blocked on
-    // b/237687047
-    afterEvaluate {
-        addCreateLibraryBuildInfoFileTasksAfterEvaluate(
-            extension.computePublishPlan(),
-            project.provider {
-                project.getFrameworksSupportCommitShaAtHead()
-            })
-    }
-}
-
-@VisibleForTesting
-fun Project.addCreateLibraryBuildInfoFileTasksAfterEvaluate(
-    info: ProjectPublishPlan,
-    shaProvider: Provider<String>
-) {
-    if (info.shouldRelease) {
-        // Only generate build info files for published libraries.
-        info.variants.forEach { variant ->
-            val task = CreateLibraryBuildInfoFileTask.setup(
-                project,
-                info.mavenGroup,
-                variant,
-                shaProvider
-            )
-
-            rootProject.tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME).configure {
-                it.dependsOn(task)
-            }
-            addTaskToAggregateBuildInfoFileTask(task)
-        }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/ProjectPublishPlan.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/ProjectPublishPlan.kt
deleted file mode 100644
index d3a6251..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/ProjectPublishPlan.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.buildInfo
-
-import androidx.build.AndroidXExtension
-import androidx.build.LibraryGroup
-import org.gradle.api.Project
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.attributes.Bundling
-import org.gradle.api.attributes.Category
-import org.gradle.api.attributes.Usage
-import org.gradle.kotlin.dsl.named
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-
-/**
- * Information extracted from a gradle project and [AndroidXExtension] that creates a view on
- * which configurations will be published, so that build_info files can parallel the maven
- * publications and contain the same dependencies
- */
-data class ProjectPublishPlan(
-    val shouldRelease: Boolean,
-    val mavenGroup: LibraryGroup?,
-    val variants: List<VariantPublishPlan>
-) {
-    /**
-     * Info about a particular variant that will be published
-     *
-     * @param artifactId the maven artifact id
-     * @param taskSuffix if non-null, will be added to the end of task names to disambiguate
-     *                   (i.e. createLibraryBuildInfoFiles becomes createLibraryBuildInfoFilesJvm)
-     * @param configurationName name of the configuration containing the dependencies for this
-     *                          variant
-     */
-    data class VariantPublishPlan(
-        val artifactId: String,
-        val taskSuffix: String? = null,
-        val configurationName: String = "releaseRuntimeElements"
-    )
-}
-
-/**
- * Compute what groups and variants will be published for this project.
- *
- * Soon, we believe we should switch this to being based directly on the publication objects,
- * rather than the underlying Configurations (b/238762087).
- */
-fun AndroidXExtension.computePublishPlan(): ProjectPublishPlan {
-    return ProjectPublishPlan(
-        shouldRelease = shouldRelease(),
-        mavenGroup = mavenGroup,
-        variants = project.configurations.filter { project.shouldBeRepresentedInBuildInfo(it) }
-            .map { config ->
-                ProjectPublishPlan.VariantPublishPlan(
-                    artifactId = config.outgoing.artifacts.single().name,
-                    taskSuffix = config.getKotlinTargetName()?.replaceFirstChar { it.uppercase() },
-                    configurationName = config.name
-                )
-            })
-}
-
-private fun Configuration.getKotlinTargetName() =
-    attributes.getAttribute(KotlinPlatformType.attribute)?.name
-
-// TODO(b/237688690): understand where -published is coming from, and have a more principled reason
-//   for excluding it, or better strategy for including it
-/*
- * Pre-KMP, we assumed that "runtimeElements" was the only configuration whose dependencies needed
- * to be tracked by JetPad.  With KMP, we need a more generic way of choosing out gradle
- * configurations.
- */
-private fun Project.shouldBeRepresentedInBuildInfo(config: Configuration) =
-    isJavaRuntime(config) &&
-        isLibrary(config) &&
-        !isShadowed(config) &&
-        config.artifacts.size == 1 &&
-        !(config.name.endsWith("-published"))
-
-private fun Project.isLibrary(config: Configuration) =
-    config.getCategory() == objects.named<Category>(Category.LIBRARY)
-
-private fun Project.isJavaRuntime(config: Configuration) =
-    config.getUsage() == objects.named<Usage>(Usage.JAVA_RUNTIME)
-
-private fun Project.isShadowed(config: Configuration) =
-    config.getBundling() == objects.named<Bundling>(Bundling.SHADOWED)
-
-private fun Configuration.getCategory() = attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
-private fun Configuration.getUsage() = attributes.getAttribute(Usage.USAGE_ATTRIBUTE)
-private fun Configuration.getBundling() = attributes.getAttribute(Bundling.BUNDLING_ATTRIBUTE)
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e51bc8b..6ae1586 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -101,9 +101,9 @@
     private fun computeArguments(): File {
 
         // path comes with colons but dokka json expects an ArrayList
-        val classPath = dependenciesClasspath.asPath.split(':').toMutableList<String>()
+        val classPath = dependenciesClasspath.asPath.split(':').toMutableList()
 
-        var linksConfiguration = ""
+        val linksConfiguration = ""
         val linksMap = mapOf(
             "coroutinesCore"
                 to "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
@@ -138,8 +138,8 @@
             )
         @Suppress("UNCHECKED_CAST")
         if (includes.isNotEmpty())
-            ((jsonMap["sourceSets"]as List<*>).single() as MutableMap<String, Any>)
-            .put("includes", includes)
+            ((jsonMap["sourceSets"]as List<*>).single() as MutableMap<String, Any>)["includes"] =
+                includes
 
         val json = JSONObject(jsonMap)
         val outputFile = File.createTempFile("dackkaArgs", ".json")
@@ -163,7 +163,6 @@
     }
 }
 
-@Suppress("UnstableApiUsage")
 interface DackkaParams : WorkParameters {
     val args: ListProperty<String>
     val classpath: SetProperty<File>
@@ -174,7 +173,6 @@
     var showLibraryMetadata: Boolean
 }
 
-@Suppress("UnstableApiUsage")
 fun runDackkaWithArgs(
     classpath: FileCollection,
     argsFile: File,
@@ -187,7 +185,7 @@
 ) {
     val workQueue = workerExecutor.noIsolation()
     workQueue.submit(DackkaWorkAction::class.java) { parameters ->
-        parameters.args.set(listOf(argsFile.getPath(), "-loggingLevel", "WARN"))
+        parameters.args.set(listOf(argsFile.path, "-loggingLevel", "WARN"))
         parameters.classpath.set(classpath)
         parameters.excludedPackages.set(excludedPackages)
         parameters.excludedPackagesForJava.set(excludedPackagesForJava)
@@ -197,7 +195,6 @@
     }
 }
 
-@Suppress("UnstableApiUsage")
 abstract class DackkaWorkAction @Inject constructor(
     private val execOperations: ExecOperations
 ) : WorkAction<DackkaParams> {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index d6224bc..b548a83 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -180,8 +180,8 @@
             val localVar = archiveOperations
             task.from(
                 sources.elements.map { jars ->
-                    jars.map {
-                        localVar.zipTree(it).matching {
+                    jars.map { jar ->
+                        localVar.zipTree(jar).matching {
                             // Filter out files that documentation tools cannot process.
                             it.exclude("**/*.MF")
                             it.exclude("**/*.aidl")
@@ -427,9 +427,9 @@
             task.dependsOn(unzipSamplesTask)
 
             val androidJar = project.getAndroidJar()
-            val dokkaClasspath = project.provider({
+            val dokkaClasspath = project.provider {
                 project.files(androidJar).plus(dependencyClasspath)
-            })
+            }
             // DokkaTask tries to resolve DokkaTask#classpath right away for jars that might not
             // be there yet. Delay the setting of this property to before we run the task.
             task.inputs.files(androidJar, dependencyClasspath)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt
deleted file mode 100644
index 77ce6aa..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.playground
-
-import androidx.build.dependencyTracker.AffectedModuleDetectorImpl
-import androidx.build.dependencyTracker.DependencyTracker
-import androidx.build.dependencyTracker.ProjectGraph
-import org.gradle.api.DefaultTask
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.options.Option
-import org.gradle.work.DisableCachingByDefault
-import java.io.File
-
-/**
- * A task to print the list of affected modules based on given parameters.
- *
- * The list of changed files can be passed via [changedFiles] property and the list of module
- * paths will be written to the given [outputFilePath].
- *
- * This task is specialized for Playground projects where any change in .github or
- * playground-common will be considered as an `INFRA` change and will be listed in the outputs.
- */
-@DisableCachingByDefault(because = "Fast to run, and declaring all inputs is difficult")
-abstract class FindAffectedModulesTask : DefaultTask() {
-    @get:Input
-    @set:Option(
-        option = "changedFilePath",
-        description = "Changed file in the build (including removed files). Can be passed " +
-            "multiple times, e.g.: --changedFilePath=a.kt --changedFilePath=b.kt " +
-            "File paths must be relative to the root directory of the main checkout"
-    )
-    abstract var changedFiles: List<String>
-
-    @get:Input
-    @set:Option(
-        option = "outputFilePath",
-        description = """
-            The output file path which will contain the list of project paths (line separated) that
-            are affected by the given list of changed files. It might also include "$INFRA_CHANGE"
-            if the change affects any of the common playground build files outside the project.
-        """
-    )
-    abstract var outputFilePath: String
-
-    @get:Input
-    abstract var projectGraph: ProjectGraph
-
-    @get:Input
-    abstract var dependencyTracker: DependencyTracker
-
-    @get:OutputFile
-    val outputFile by lazy {
-        File(outputFilePath)
-    }
-
-    init {
-        group = "Tooling"
-        description = """
-            Outputs the list of projects in the playground project that are affected by the
-            given list of files.
-            ./gradlew findAffectedModules --changedFilePath=file1 --changedFilePath=file2 \
-                      --outputFilePath=`pwd`/changes.txt
-        """.trimIndent()
-    }
-
-    @TaskAction
-    fun checkAffectedModules() {
-        val hasChangedGithubInfraFiles = changedFiles.any {
-            it.contains(".github") ||
-                it.contains("playground-common") ||
-                it.contains("buildSrc")
-        }
-        val detector = AffectedModuleDetectorImpl(
-            projectGraph = projectGraph,
-            dependencyTracker = dependencyTracker,
-            logger = logger,
-            cobuiltTestPaths = setOf<Set<String>>(),
-            changedFilesProvider = {
-                changedFiles
-            }
-        )
-        val changedProjectPaths = detector.affectedProjects.map {
-            it
-        } + if (hasChangedGithubInfraFiles) {
-            listOf(INFRA_CHANGE)
-        } else {
-            emptyList()
-        }
-        check(outputFile.parentFile?.exists() == true) {
-            "invalid output file argument: $outputFile. Make sure to pass an absolute path"
-        }
-        val changedProjects = changedProjectPaths.joinToString(System.lineSeparator())
-        outputFile.writeText(changedProjects, charset = Charsets.UTF_8)
-        logger.info("putting result $changedProjects into ${outputFile.absolutePath}")
-    }
-
-    companion object {
-        /**
-         * Denotes that the changes affect common playground build files / configuration.
-         */
-        const val INFRA_CHANGE = "INFRA"
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 77ea567..21dc7a8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1915,7 +1915,7 @@
          * A rotation of 90 degrees would mean rotating the image 90 degrees clockwise produces an
          * image that will match the display orientation.
          *
-         * <p>See also {@link Builder#setTargetRotation(int)} and
+         * <p>See also {@link ImageCapture.Builder#setTargetRotation(int)} and
          * {@link #setTargetRotation(int)}.
          *
          * <p>Timestamps are in nanoseconds and monotonic and can be compared to timestamps from
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
index ae4ae00..44e2abd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.impl;
 
+import android.hardware.camera2.CaptureRequest;
 import android.util.ArrayMap;
 import android.util.Pair;
 
@@ -40,6 +41,9 @@
 
     private static final TagBundle EMPTY_TAGBUNDLE = new TagBundle(new ArrayMap<>());
 
+    private static final String USER_TAG_PREFIX = "android.hardware.camera2.CaptureRequest.setTag.";
+
+    private static final String CAMERAX_USER_TAG_PREFIX = USER_TAG_PREFIX + "CX";
     /**
      * Creates an empty TagBundle.
      *
@@ -101,4 +105,24 @@
     public Set<String> listKeys() {
         return mTagMap.keySet();
     }
+
+    /**
+     * Produces a string that can be used to identify CameraX usage in a Camera2
+     * {@link CaptureRequest}.
+     *
+     * <p>In Android 13 or later, Camera2 will log the string representation of any
+     * tag set on {@link CaptureRequest.Builder#setTag(Object)}. Since
+     * tag bundles are always set internally by CameraX as the tag in a capture
+     * request, the constant string value returned here can be used to identify
+     * usage of CameraX versus application usage of Camera2.
+     *
+     * <p>Note: Doesn't return an actual string representation of the tag bundle.
+     *
+     * @return Returns a constant string value used to identify usage of CameraX.
+     */
+    @NonNull
+    @Override
+    public final String toString() {
+        return CAMERAX_USER_TAG_PREFIX;
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
index e204317..3edf244 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
@@ -18,9 +18,15 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 
-/** A provider that supplies OpenGL shader code. */
-interface ShaderProvider {
+/**
+ * A provider that supplies OpenGL shader code.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ShaderProvider {
 
     /**
      * Creates the fragment shader code with the given variable names.
@@ -34,14 +40,15 @@
      *         varying vec2 {$fragCoordsVarName};
      *         void main() {
      *           vec4 sampleColor = texture2D({$samplerVarName}, {$fragCoordsVarName});
-     *           gl_FragColor = vec4(sampleColor.r * 0.493 + sampleColor. g * 0.769 +
-     *              sampleColor.b * 0.289, sampleColor.r * 0.449 + sampleColor.g * 0.686 +
-     *              sampleColor.b * 0.268, sampleColor.r * 0.272 + sampleColor.g * 0.534 +
-     *              sampleColor.b * 0.131, 1.0);
+     *           gl_FragColor = vec4(
+     *               sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+     *               sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+     *               sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+     *               1.0);
      *         }
      * }</pre>
      *
-     * @param samplerVarName the variable name of the samplerExternalOES.
+     * @param samplerVarName    the variable name of the samplerExternalOES.
      * @param fragCoordsVarName the variable name of the fragment coordinates.
      * @return the shader code. Return null to use the default shader.
      */
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
index 85572f1..616fb90 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
@@ -19,6 +19,7 @@
 import android.os.Build
 import androidx.camera.core.SurfaceEffect.PREVIEW
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.testing.fakes.FakeSurfaceEffect
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,11 +42,7 @@
 
     @Test(expected = IllegalArgumentException::class)
     fun addMoreThanOnePreviewEffect_throwsException() {
-        val surfaceEffect = object : SurfaceEffect {
-            override fun onInputSurface(request: SurfaceRequest) {}
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
-        }
+        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
         EffectBundle.Builder(mainThreadExecutor())
             .addEffect(PREVIEW, surfaceEffect)
             .addEffect(PREVIEW, surfaceEffect)
@@ -54,11 +51,7 @@
     @Test
     fun addPreviewEffect_hasPreviewEffect() {
         // Arrange.
-        val surfaceEffect = object : SurfaceEffect {
-            override fun onInputSurface(request: SurfaceRequest) {}
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
-        }
+        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
         // Act.
         val effectBundle = EffectBundle.Builder(mainThreadExecutor())
             .addEffect(PREVIEW, surfaceEffect)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 1be7cf1..0cae6de 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -42,6 +42,7 @@
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
 import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -71,8 +72,6 @@
 
     private lateinit var appSurface: Surface
     private lateinit var appSurfaceTexture: SurfaceTexture
-    private lateinit var effectSurface: Surface
-    private lateinit var effectSurfaceTexture: SurfaceTexture
     private lateinit var camera: FakeCamera
     private lateinit var cameraXConfig: CameraXConfig
     private lateinit var context: Context
@@ -82,8 +81,6 @@
     fun setUp() {
         appSurfaceTexture = SurfaceTexture(0)
         appSurface = Surface(appSurfaceTexture)
-        effectSurfaceTexture = SurfaceTexture(0)
-        effectSurface = Surface(effectSurfaceTexture)
         camera = FakeCamera()
 
         val cameraFactoryProvider =
@@ -106,8 +103,6 @@
     fun tearDown() {
         appSurfaceTexture.release()
         appSurface.release()
-        effectSurfaceTexture.release()
-        effectSurface.release()
         with(cameraUseCaseAdapter) {
             this?.removeUseCases(useCases)
         }
@@ -222,27 +217,10 @@
     @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
-        var surfaceOutputReceived: SurfaceOutput? = null
-        var effectSurfaceReadyToRelease = false
-        var isEffectReleased = false
-        val surfaceEffect = object : SurfaceEffectInternal {
-            override fun onInputSurface(request: SurfaceRequest) {
-                request.provideSurface(effectSurface, mainThreadExecutor()) {
-                    effectSurfaceReadyToRelease = true
-                }
-            }
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
-                surfaceOutputReceived = surfaceOutput
-            }
-
-            override fun release() {
-                isEffectReleased = true
-            }
-        }
+        val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
 
         // Act: create pipeline in Preview and provide Surface.
-        val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
+        val preview = createPreviewPipelineAndAttachEffect(effect)
         val surfaceRequest = preview.mCurrentSurfaceRequest!!
         var appSurfaceReadyToRelease = false
         surfaceRequest.provideSurface(appSurface, mainThreadExecutor()) {
@@ -251,30 +229,26 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: surfaceOutput received.
-        assertThat(surfaceOutputReceived).isNotNull()
-        var requestedToReleaseOutputSurface = false
-        surfaceOutputReceived!!.getSurface(mainThreadExecutor()) {
-            requestedToReleaseOutputSurface = true
-        }
-        assertThat(isEffectReleased).isFalse()
-        assertThat(requestedToReleaseOutputSurface).isFalse()
-        assertThat(effectSurfaceReadyToRelease).isFalse()
+        assertThat(effect.surfaceOutput).isNotNull()
+        assertThat(effect.isReleased).isFalse()
+        assertThat(effect.isOutputSurfaceRequestedToClose).isFalse()
+        assertThat(effect.isInputSurfaceReleased).isFalse()
         assertThat(appSurfaceReadyToRelease).isFalse()
         // effect surface is provided to camera.
-        assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effectSurface)
+        assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effect.inputSurface)
 
         // Act: unbind Preview.
         preview.onDetached()
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect and effect surface is released.
-        assertThat(isEffectReleased).isTrue()
-        assertThat(requestedToReleaseOutputSurface).isTrue()
-        assertThat(effectSurfaceReadyToRelease).isTrue()
+        assertThat(effect.isReleased).isTrue()
+        assertThat(effect.isOutputSurfaceRequestedToClose).isTrue()
+        assertThat(effect.isInputSurfaceReleased).isTrue()
         assertThat(appSurfaceReadyToRelease).isFalse()
 
         // Act: close SurfaceOutput
-        surfaceOutputReceived!!.close()
+        effect.surfaceOutput!!.close()
         shadowOf(getMainLooper()).idle()
         assertThat(appSurfaceReadyToRelease).isTrue()
     }
@@ -282,18 +256,8 @@
     @Test
     fun invokedErrorListener_recreatePipeline() {
         // Arrange: create pipeline and get a reference of the SessionConfig.
-        val surfaceEffect = object : SurfaceEffectInternal {
-            override fun onInputSurface(request: SurfaceRequest) {}
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
-                surfaceOutput.getSurface(mainThreadExecutor()) {
-                    surfaceOutput.close()
-                }
-            }
-
-            override fun release() {}
-        }
-        val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
+        val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
+        val preview = createPreviewPipelineAndAttachEffect(effect)
         val originalSessionConfig = preview.sessionConfig
 
         // Act: invoke the error listener.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
index 37ea1bf..1896e6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
@@ -26,8 +26,6 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
@@ -44,13 +42,6 @@
     private static final Integer TAG_VALUE_2 = 2;
 
     TagBundle mTagBundle;
-    private static final List<String> KEY_LIST = new ArrayList<>();
-
-    static {
-        KEY_LIST.add(TAG_0);
-        KEY_LIST.add(TAG_1);
-        KEY_LIST.add(TAG_2);
-    }
 
     @Before
     public void setUp() {
@@ -84,4 +75,10 @@
 
         assertThat(keyList).containsExactly(TAG_0, TAG_1, TAG_2);
     }
+
+    @Test
+    public void verifyTagBundleToString() {
+        assertThat(mTagBundle.toString()).startsWith("android.hardware.camera2.CaptureRequest"
+                + ".setTag.CX");
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 42f71acb..951f021 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -19,11 +19,10 @@
 import android.os.Build
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.processing.SurfaceEffectWithExecutor
+import androidx.camera.testing.fakes.FakeSurfaceEffect
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
@@ -43,22 +42,20 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class CameraUseCaseAdapterTest {
 
-    private lateinit var surfaceEffect: SurfaceEffect
+    private lateinit var surfaceEffect: FakeSurfaceEffect
     private lateinit var mEffectBundle: EffectBundle
     private lateinit var executor: ExecutorService
 
     @Before
     fun setUp() {
-        surfaceEffect = object : SurfaceEffect {
-            override fun onInputSurface(request: SurfaceRequest) {}
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
-        }
+        surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
         executor = Executors.newSingleThreadExecutor()
         mEffectBundle = EffectBundle.Builder(executor).addEffect(PREVIEW, surfaceEffect).build()
     }
 
     @After
     fun tearDown() {
+        surfaceEffect.cleanUp()
         executor.shutdown()
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
index ca79bdea..7df85dd 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
@@ -24,11 +24,10 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -55,15 +54,9 @@
         private val CROP_RECT = Rect(0, 0, 600, 400)
     }
 
-    private lateinit var surfaceEffect: SurfaceEffectInternal
-    private var isReleased = false
-    private var surfaceOutputCloseRequested = false
-    private var surfaceOutputReceived: SurfaceOutput? = null
-    private var surfaceReceivedByEffect: Surface? = null
+    private lateinit var surfaceEffectInternal: FakeSurfaceEffectInternal
     private lateinit var appSurface: Surface
     private lateinit var appSurfaceTexture: SurfaceTexture
-    private lateinit var effectSurface: Surface
-    private lateinit var effectSurfaceTexture: SurfaceTexture
     private lateinit var node: SurfaceEffectNode
     private lateinit var inputEdge: SurfaceEdge
 
@@ -71,30 +64,8 @@
     fun setup() {
         appSurfaceTexture = SurfaceTexture(0)
         appSurface = Surface(appSurfaceTexture)
-        effectSurfaceTexture = SurfaceTexture(0)
-        effectSurface = Surface(effectSurfaceTexture)
-
-        surfaceEffect = object : SurfaceEffectInternal {
-            override fun onInputSurface(request: SurfaceRequest) {
-                request.provideSurface(effectSurface, mainThreadExecutor()) {
-                    effectSurfaceTexture.release()
-                    effectSurface.release()
-                }
-            }
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
-                surfaceOutputReceived = surfaceOutput
-                surfaceReceivedByEffect = surfaceOutput.getSurface(mainThreadExecutor()) {
-                    surfaceOutput.close()
-                    surfaceOutputCloseRequested = true
-                }
-            }
-
-            override fun release() {
-                isReleased = true
-            }
-        }
-        node = SurfaceEffectNode(FakeCamera(), surfaceEffect)
+        surfaceEffectInternal = FakeSurfaceEffectInternal(mainThreadExecutor())
+        node = SurfaceEffectNode(FakeCamera(), surfaceEffectInternal)
         inputEdge = createInputEdge()
     }
 
@@ -102,8 +73,7 @@
     fun tearDown() {
         appSurfaceTexture.release()
         appSurface.release()
-        effectSurfaceTexture.release()
-        effectSurface.release()
+        surfaceEffectInternal.release()
         node.release()
         inputEdge.surfaces[0].close()
         shadowOf(getMainLooper()).idle()
@@ -140,8 +110,8 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect receives app Surface. CameraX receives effect Surface.
-        assertThat(surfaceReceivedByEffect).isEqualTo(appSurface)
-        assertThat(inputSurface.surface.get()).isEqualTo(effectSurface)
+        assertThat(surfaceEffectInternal.outputSurface).isEqualTo(appSurface)
+        assertThat(inputSurface.surface.get()).isEqualTo(surfaceEffectInternal.inputSurface)
     }
 
     @Test
@@ -156,8 +126,8 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect is released and has requested effect to close the SurfaceOutput
-        assertThat(isReleased).isTrue()
-        assertThat(surfaceOutputCloseRequested).isTrue()
+        assertThat(surfaceEffectInternal.isReleased).isTrue()
+        assertThat(surfaceEffectInternal.isOutputSurfaceRequestedToClose).isTrue()
     }
 
     private fun createInputEdge(): SurfaceEdge {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
index 86b492e..6a33afe 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import com.google.common.truth.Truth.assertThat
 import java.lang.Thread.currentThread
 import java.util.concurrent.Executor
@@ -69,13 +70,10 @@
 
     @Test(expected = IllegalStateException::class)
     fun initWithSurfaceEffectInternal_throwsException() {
-        SurfaceEffectWithExecutor(object : SurfaceEffectInternal {
-            override fun onInputSurface(request: SurfaceRequest) {}
-
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
-
-            override fun release() {}
-        }, mainThreadExecutor())
+        SurfaceEffectWithExecutor(
+            FakeSurfaceEffectInternal(mainThreadExecutor()),
+            mainThreadExecutor()
+        )
     }
 
     @Test
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index bf87a7f..35e8e77 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,10 +26,7 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
@@ -40,6 +37,7 @@
 import androidx.camera.testing.fakes.FakeCameraFactory
 import androidx.camera.testing.fakes.FakeCameraInfoInternal
 import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.camera.testing.fakes.FakeSurfaceEffect
 import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
@@ -81,12 +79,7 @@
     fun bindUseCaseGroupWithEffect_effectIsSetOnUseCase() {
         // Arrange.
         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
-        val surfaceEffect = object : SurfaceEffect {
-            override fun onInputSurface(request: SurfaceRequest) {}
-            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
-                surfaceOutput.close()
-            }
-        }
+        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
         val effectBundle =
             EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
         val preview = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
new file mode 100644
index 0000000..1287dee
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.graphics.SurfaceTexture;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.SurfaceEffect;
+import androidx.camera.core.SurfaceOutput;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.impl.DeferrableSurface;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Fake {@link SurfaceEffect} used in tests.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class FakeSurfaceEffect implements SurfaceEffect {
+
+    final SurfaceTexture mSurfaceTexture;
+    final Surface mInputSurface;
+    private final Executor mExecutor;
+
+
+    @Nullable
+    private SurfaceRequest mSurfaceRequest;
+    @Nullable
+    private SurfaceOutput mSurfaceOutput;
+    boolean mIsInputSurfaceReleased;
+    boolean mIsOutputSurfaceRequestedToClose;
+
+    Surface mOutputSurface;
+
+    public FakeSurfaceEffect(@NonNull Executor executor) {
+        mSurfaceTexture = new SurfaceTexture(0);
+        mInputSurface = new Surface(mSurfaceTexture);
+        mExecutor = executor;
+        mIsInputSurfaceReleased = false;
+        mIsOutputSurfaceRequestedToClose = false;
+    }
+
+    @Override
+    public void onInputSurface(@NonNull SurfaceRequest request) {
+        mSurfaceRequest = request;
+        request.provideSurface(mInputSurface, mExecutor, result -> {
+            mSurfaceTexture.release();
+            mInputSurface.release();
+            mIsInputSurfaceReleased = true;
+        });
+    }
+
+    @Override
+    public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
+        mSurfaceOutput = surfaceOutput;
+        mOutputSurface = surfaceOutput.getSurface(mExecutor,
+                () -> mIsOutputSurfaceRequestedToClose = true);
+    }
+
+    @Nullable
+    public SurfaceRequest getSurfaceRequest() {
+        return mSurfaceRequest;
+    }
+
+    @Nullable
+    public SurfaceOutput getSurfaceOutput() {
+        return mSurfaceOutput;
+    }
+
+    @NonNull
+    public Surface getInputSurface() {
+        return mInputSurface;
+    }
+
+    @NonNull
+    public Surface getOutputSurface() {
+        return mOutputSurface;
+    }
+
+    public boolean isInputSurfaceReleased() {
+        return mIsInputSurfaceReleased;
+    }
+
+    public boolean isOutputSurfaceRequestedToClose() {
+        return mIsOutputSurfaceRequestedToClose;
+    }
+
+    /**
+     * Clear up the instance to avoid the "{@link DeferrableSurface} garbage collected" error.
+     */
+    public void cleanUp() {
+        if (mSurfaceRequest != null) {
+            mSurfaceRequest.willNotProvideSurface();
+        }
+        if (mSurfaceOutput != null) {
+            mSurfaceOutput.close();
+        }
+        mSurfaceTexture.release();
+        mInputSurface.release();
+    }
+
+    @Override
+    protected void finalize() {
+        cleanUp();
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
new file mode 100644
index 0000000..47bd6f7
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.processing.SurfaceEffectInternal;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Fake {@link SurfaceEffectInternal} used in tests.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class FakeSurfaceEffectInternal extends FakeSurfaceEffect implements SurfaceEffectInternal {
+
+    private boolean mIsReleased;
+
+    public FakeSurfaceEffectInternal(@NonNull Executor executor) {
+        super(executor);
+        mIsReleased = false;
+    }
+
+    public boolean isReleased() {
+        return mIsReleased;
+    }
+
+    @Override
+    public void release() {
+        mIsReleased = true;
+    }
+}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
index c478811..2e5c7b8 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
@@ -25,10 +25,7 @@
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.ImageCapture
-import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -36,6 +33,7 @@
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.fakes.FakeActivity
 import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.camera.testing.fakes.FakeSurfaceEffect
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
@@ -105,15 +103,10 @@
 
         // Act: set an EffectBundle
         instrumentation.runOnMainSync {
-            val surfaceEffect = object : SurfaceEffect {
-                override fun onInputSurface(request: SurfaceRequest) {}
-
-                override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
-                    surfaceOutput.close()
-                }
-            }
             controller.setEffectBundle(
-                EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
+                EffectBundle.Builder(mainThreadExecutor())
+                    .addEffect(PREVIEW, FakeSurfaceEffect(mainThreadExecutor()))
+                    .build()
             )
         }
 
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
index aefea06..5d146b13 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
@@ -28,6 +28,8 @@
 import android.content.Intent;
 
 import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
+import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.Preview;
 import androidx.camera.testing.CameraUtil;
@@ -35,7 +37,6 @@
 import androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.IdlingRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
@@ -48,11 +49,16 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 import leakcanary.FailTestOnLeak;
 
 // Tests basic UI operation when using CoreTest app.
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
 @LargeTest
 public final class BasicUITest {
     private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
@@ -63,13 +69,24 @@
     private final Intent mIntent = mContext.getPackageManager()
             .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
 
+    @Parameterized.Parameter(0)
+    public CameraXConfig mCameraConfig;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> getParameters() {
+        List<Object[]> result = new ArrayList<>();
+        result.add(new Object[]{Camera2Config.defaultConfig()});
+        result.add(new Object[]{CameraPipeConfig.INSTANCE.defaultConfig()});
+        return result;
+    }
+
     @Rule
     public ActivityTestRule<CameraXActivity> mActivityRule =
             new ActivityTestRule<>(CameraXActivity.class, true, false);
 
     @Rule
     public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
-            new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+            new CameraUtil.PreTestCameraIdList(mCameraConfig)
     );
 
     @Rule
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
similarity index 93%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
rename to camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index a351980..f61bdc6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,18 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.camera.camera2
+package androidx.camera.integration.core.camera2
 
 import android.content.Context
 import android.graphics.SurfaceTexture
 import android.util.Size
 import android.view.Surface
+import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -37,7 +39,6 @@
 import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
 import androidx.core.util.Consumer
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
@@ -50,24 +51,40 @@
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeoutException
 import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
+import org.junit.runners.Parameterized
 
 @LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewTest {
+class PreviewTest(
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
     @get:Rule
     val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
+        PreTestCameraIdList(cameraConfig)
     )
+
+    companion object {
+        private const val ANY_THREAD_NAME = "any-thread-name"
+        private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
     private var defaultBuilder: Preview.Builder? = null
@@ -81,8 +98,7 @@
     @Throws(ExecutionException::class, InterruptedException::class)
     fun setUp() {
         context = ApplicationProvider.getApplicationContext()
-        val cameraXConfig = Camera2Config.defaultConfig()
-        CameraXUtil.initialize(context!!, cameraXConfig).get()
+        CameraXUtil.initialize(context!!, cameraConfig).get()
 
         // init CameraX before creating Preview to get preview size with CameraX's context
         defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
@@ -106,38 +122,30 @@
     }
 
     @Test
-    fun surfaceProvider_isUsedAfterSetting() {
-        val surfaceProvider = Mockito.mock(
-            Preview.SurfaceProvider::class.java
-        )
-        Mockito.doAnswer { args: InvocationOnMock ->
-            val surfaceTexture = SurfaceTexture(0)
-            surfaceTexture.setDefaultBufferSize(640, 480)
-            val surface = Surface(surfaceTexture)
-            (args.getArgument<Any>(0) as SurfaceRequest).provideSurface(
-                surface,
-                CameraXExecutors.directExecutor()
-            ) {
-                surfaceTexture.release()
-                surface.release()
-            }
-            null
-        }.`when`(surfaceProvider).onSurfaceRequested(
-            ArgumentMatchers.any(
-                SurfaceRequest::class.java
-            )
-        )
+    fun surfaceProvider_isUsedAfterSetting() = runBlocking {
         val preview = defaultBuilder!!.build()
+        val completableDeferred = CompletableDeferred<Unit>()
 
         // TODO(b/160261462) move off of main thread when setSurfaceProvider does not need to be
         //  done on the main thread
-        instrumentation.runOnMainSync { preview.setSurfaceProvider(surfaceProvider) }
-        camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
-        Mockito.verify(surfaceProvider, Mockito.timeout(3000)).onSurfaceRequested(
-            ArgumentMatchers.any(
-                SurfaceRequest::class.java
+        instrumentation.runOnMainSync { preview.setSurfaceProvider { request ->
+            val surfaceTexture = SurfaceTexture(0)
+            surfaceTexture.setDefaultBufferSize(
+                request.resolution.width,
+                request.resolution.height
             )
-        )
+            surfaceTexture.detachFromGLContext()
+            val surface = Surface(surfaceTexture)
+            request.provideSurface(surface, CameraXExecutors.directExecutor()) {
+                surface.release()
+                surfaceTexture.release()
+            }
+            completableDeferred.complete(Unit)
+        } }
+        camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
+        withTimeout(3_000) {
+            completableDeferred.await()
+        }
     }
 
     @Test
@@ -564,9 +572,4 @@
         } while (totalCheckTime < timeoutMs)
         return false
     }
-
-    companion object {
-        private const val ANY_THREAD_NAME = "any-thread-name"
-        private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
-    }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 28fe8db..408f301 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -135,6 +135,21 @@
     }
 
     @Test
+    fun enableEffect_effectIsEnabled() {
+        // Arrange: launch app and verify effect is inactive.
+        fragment.assertPreviewIsStreaming()
+        assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isFalse()
+
+        // Act: turn on effect.
+        val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+        uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+        instrumentation.waitForIdleSync()
+
+        // Assert: verify that effect is active.
+        assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isTrue()
+    }
+
+    @Test
     fun controllerBound_canGetCameraControl() {
         fragment.assertPreviewIsStreaming()
         instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e765274..14833f1 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.integration.view;
 
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -40,12 +42,13 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.EffectBundle;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceEffect;
 import androidx.camera.core.ZoomState;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.view.CameraController;
@@ -86,6 +89,7 @@
     private FrameLayout mContainer;
     private Button mFlashMode;
     private ToggleButton mCameraToggle;
+    private ToggleButton mEffectToggle;
     private ExecutorService mExecutorService;
     private ToggleButton mCaptureEnabledToggle;
     private ToggleButton mAnalysisEnabledToggle;
@@ -106,6 +110,9 @@
     @Nullable
     private ImageAnalysis.Analyzer mWrappedAnalyzer;
 
+    @VisibleForTesting
+    ToneMappingSurfaceEffect mSurfaceEffect;
+
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
         image.getPlanes()[0].getBuffer().get(bytes);
@@ -134,7 +141,7 @@
         mExecutorService = Executors.newSingleThreadExecutor();
         mRotationProvider = new RotationProvider(requireContext());
         boolean canDetectRotation = mRotationProvider.addListener(
-                CameraXExecutors.mainThreadExecutor(), mRotationListener);
+                mainThreadExecutor(), mRotationListener);
         if (!canDetectRotation) {
             Logger.e(TAG, "The device cannot detect rotation with motion sensor.");
         }
@@ -159,6 +166,12 @@
             }
         });
 
+        // Set up post-processing effects.
+        mSurfaceEffect = new ToneMappingSurfaceEffect();
+        mEffectToggle = view.findViewById(R.id.effect_toggle);
+        mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
+        onEffectsToggled();
+
         // Set up the button to change the PreviewView's size.
         view.findViewById(R.id.shrink).setOnClickListener(v -> {
             // Shrinks PreviewView by 10% each time it's clicked.
@@ -341,6 +354,17 @@
             mExecutorService.shutdown();
         }
         mRotationProvider.removeListener(mRotationListener);
+        mSurfaceEffect.release();
+    }
+
+    private void onEffectsToggled() {
+        if (mEffectToggle.isChecked()) {
+            mCameraController.setEffectBundle(new EffectBundle.Builder(mainThreadExecutor())
+                    .addEffect(SurfaceEffect.PREVIEW, mSurfaceEffect)
+                    .build());
+        } else if (mSurfaceEffect != null) {
+            mCameraController.setEffectBundle(null);
+        }
     }
 
     void checkFailedFuture(ListenableFuture<Void> voidFuture) {
@@ -355,7 +379,7 @@
             public void onFailure(@NonNull Throwable t) {
                 toast(t.getMessage());
             }
-        }, CameraXExecutors.mainThreadExecutor());
+        }, mainThreadExecutor());
     }
 
     // Synthetic access
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
new file mode 100644
index 0000000..2ed13ae
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view
+
+import android.graphics.SurfaceTexture
+import android.graphics.SurfaceTexture.OnFrameAvailableListener
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.SurfaceEffect
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.checkMainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.processing.OpenGlRenderer
+import androidx.camera.core.processing.ShaderProvider
+
+/**
+ * A effect that applies tone mapping on camera output.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+class ToneMappingSurfaceEffect : SurfaceEffect, OnFrameAvailableListener {
+
+    companion object {
+        // A fragment shader that applies a yellow hue.
+        private val TONE_MAPPING_SHADER_PROVIDER = object : ShaderProvider {
+            override fun createFragmentShader(sampler: String, fragCoords: String): String {
+                return """
+                    #extension GL_OES_EGL_image_external : require
+                    precision mediump float;
+                    uniform samplerExternalOES $sampler;
+                    varying vec2 $fragCoords;
+                    void main() {
+                      vec4 sampleColor = texture2D($sampler, $fragCoords);
+                      gl_FragColor = vec4(
+                           sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+                           sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+                           sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+                           1.0);
+                     }
+                    """
+            }
+        }
+    }
+
+    private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+    private val glRenderer: OpenGlRenderer = OpenGlRenderer()
+    private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
+    private val textureTransform: FloatArray = FloatArray(16)
+    private val surfaceTransform: FloatArray = FloatArray(16)
+    private var isReleased = false
+
+    // For testing.
+    private var surfaceRequested = false
+    // For testing.
+    private var outputSurfaceProvided = false
+
+    init {
+        mainThreadExecutor().execute {
+            glRenderer.init(TONE_MAPPING_SHADER_PROVIDER)
+        }
+    }
+
+    override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+        checkMainThread()
+        if (isReleased) {
+            surfaceRequest.willNotProvideSurface()
+            return
+        }
+        surfaceRequested = true
+        val surfaceTexture = SurfaceTexture(glRenderer.textureName)
+        surfaceTexture.setDefaultBufferSize(
+            surfaceRequest.resolution.width, surfaceRequest.resolution.height
+        )
+        val surface = Surface(surfaceTexture)
+        surfaceRequest.provideSurface(surface, mainThreadExecutor()) {
+            surfaceTexture.setOnFrameAvailableListener(null)
+            surfaceTexture.release()
+            surface.release()
+        }
+        surfaceTexture.setOnFrameAvailableListener(this, mainThreadHandler)
+    }
+
+    override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+        checkMainThread()
+        outputSurfaceProvided = true
+        if (isReleased) {
+            surfaceOutput.close()
+            return
+        }
+        outputSurfaces[surfaceOutput] = surfaceOutput.getSurface(mainThreadExecutor()) {
+            surfaceOutput.close()
+            outputSurfaces.remove(surfaceOutput)
+        }
+    }
+
+    @VisibleForTesting
+    fun isSurfaceRequestedAndProvided(): Boolean {
+        return surfaceRequested && outputSurfaceProvided
+    }
+
+    fun release() {
+        checkMainThread()
+        if (isReleased) {
+            return
+        }
+        glRenderer.release()
+        isReleased = true
+    }
+
+    override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
+        checkMainThread()
+        if (isReleased) {
+            return
+        }
+        surfaceTexture.updateTexImage()
+        surfaceTexture.getTransformMatrix(textureTransform)
+        for (entry in outputSurfaces.entries.iterator()) {
+            val surface = entry.value
+            val surfaceOutput = entry.key
+            glRenderer.setOutputSurface(surface)
+            surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
+            glRenderer.render(surfaceTexture.timestamp, surfaceTransform)
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
index 1bce16f..47fad34 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
@@ -51,6 +51,12 @@
                 android:layout_height="wrap_content"
                 android:textOff="@string/toggle_camera_front"
                 android:textOn="@string/toggle_camera_back" />
+            <ToggleButton
+                android:id="@+id/effect_toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textOff="@string/toggle_effect_off"
+                android:textOn="@string/toggle_effect_on" />
         </LinearLayout>
 
         <LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
index 9b97b3f..e7680b2 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
@@ -48,6 +48,12 @@
             android:layout_height="wrap_content"
             android:textOff="@string/toggle_camera_front"
             android:textOn="@string/toggle_camera_back" />
+        <ToggleButton
+            android:id="@+id/effect_toggle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOff="@string/toggle_effect_off"
+            android:textOn="@string/toggle_effect_on" />
     </LinearLayout>
 
     <LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
index d4d6509..01ea7da 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -36,6 +36,8 @@
     <string name="toggle_analyzer_not_set">Analyzer not set</string>
     <string name="toggle_camera_front">Front</string>
     <string name="toggle_camera_back">Back</string>
+    <string name="toggle_effect_on">Effect On</string>
+    <string name="toggle_effect_off">Effect Off</string>
     <string name="btn_remove_or_add">Remove/Add</string>
     <string name="btn_shrink">Shrink</string>
     <string name="btn_switch">Switch</string>
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 254933a..27223c7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -285,6 +285,7 @@
   public final class ScrollableDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
+    method @androidx.compose.foundation.ExperimentalFoundationApi public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
     field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
   }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index afc8293..3428f11 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -54,7 +54,6 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.verticalScrollAxisRange
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.LayoutDirection
 import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
@@ -286,17 +285,11 @@
         val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
         val scrolling = Modifier.scrollable(
             orientation = orientation,
-            reverseDirection = run {
-                // A finger moves with the content, not with the viewport. Therefore,
-                // always reverse once to have "natural" gesture that goes reversed to layout
-                var reverseDirection = !reverseScrolling
-                // But if rtl and horizontal, things move the other way around
-                val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-                if (isRtl && !isVertical) {
-                    reverseDirection = !reverseDirection
-                }
-                reverseDirection
-            },
+            reverseDirection = ScrollableDefaults.reverseDirection(
+                LocalLayoutDirection.current,
+                orientation,
+                reverseScrolling
+            ),
             enabled = isScrollable,
             interactionSource = state.internalInteractionSource,
             flingBehavior = flingBehavior,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7b54efd..c882690 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -59,9 +59,11 @@
 import androidx.compose.ui.layout.OnRemeasuredModifier
 import androidx.compose.ui.modifier.ModifierLocalProvider
 import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.util.fastAll
@@ -211,6 +213,33 @@
     fun overscrollEffect(): OverscrollEffect {
         return rememberOverscrollEffect()
     }
+
+    /**
+     * Used to determine the value of `reverseDirection` parameter of [Modifier.scrollable]
+     * in scrollable layouts.
+     *
+     * @param layoutDirection current layout direction (e.g. from [LocalLayoutDirection])
+     * @param orientation orientation of scroll
+     * @param reverseScrolling whether scrolling direction should be reversed
+     *
+     * @return `true` if scroll direction should be reversed, `false` otherwise.
+     */
+    @ExperimentalFoundationApi
+    fun reverseDirection(
+        layoutDirection: LayoutDirection,
+        orientation: Orientation,
+        reverseScrolling: Boolean
+    ): Boolean {
+        // A finger moves with the content, not with the viewport. Therefore,
+        // always reverse once to have "natural" gesture that goes reversed to layout
+        var reverseDirection = !reverseScrolling
+        // But if rtl and horizontal, things move the other way around
+        val isRtl = layoutDirection == LayoutDirection.Rtl
+        if (isRtl && orientation != Orientation.Vertical) {
+            reverseDirection = !reverseDirection
+        }
+        return reverseDirection
+    }
 }
 
 internal interface ScrollConfig {
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 e205b5e..b918123 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
@@ -41,7 +41,6 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.offset
@@ -119,17 +118,11 @@
             .overscroll(overscrollEffect)
             .scrollable(
                 orientation = orientation,
-                reverseDirection = run {
-                    // A finger moves with the content, not with the viewport. Therefore,
-                    // always reverse once to have "natural" gesture that goes reversed to layout
-                    var reverseDirection = !reverseLayout
-                    // But if rtl and horizontal, things move the other way around
-                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-                    if (isRtl && !isVertical) {
-                        reverseDirection = !reverseDirection
-                    }
-                    reverseDirection
-                },
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
                 interactionSource = state.internalInteractionSource,
                 flingBehavior = flingBehavior,
                 state = state,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 1c41760..16f698d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -41,7 +41,6 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
@@ -118,17 +117,11 @@
             .overscroll(overscrollEffect)
             .scrollable(
                 orientation = orientation,
-                reverseDirection = run {
-                    // A finger moves with the content, not with the viewport. Therefore,
-                    // always reverse once to have "natural" gesture that goes reversed to layout
-                    var reverseDirection = !reverseLayout
-                    // But if rtl and horizontal, things move the other way around
-                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-                    if (isRtl && !isVertical) {
-                        reverseDirection = !reverseDirection
-                    }
-                    reverseDirection
-                },
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
                 interactionSource = state.internalInteractionSource,
                 flingBehavior = flingBehavior,
                 state = state,
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 7a57b3e..88e4217 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -8,12 +8,12 @@
     method @androidx.compose.runtime.Composable public long getTextContentColor();
     method @androidx.compose.runtime.Composable public long getTitleContentColor();
     method public float getTonalElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long IconContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final long TextContentColor;
-    property @androidx.compose.runtime.Composable public final long TitleContentColor;
     property public final float TonalElevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long iconContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final long textContentColor;
+    property @androidx.compose.runtime.Composable public final long titleContentColor;
     field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
   }
 
@@ -36,7 +36,7 @@
 
   public final class BadgeDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
   }
 
@@ -46,26 +46,21 @@
   }
 
   public final class BottomAppBarDefaults {
-    method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float ContainerElevation;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+    property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
-  public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
-    method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+  public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
-    property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
-    field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+    field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
   }
 
   @androidx.compose.runtime.Stable public interface ButtonColors {
@@ -97,17 +92,17 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
     property public final float IconSize;
     property public final float IconSpacing;
     property public final float MinHeight;
     property public final float MinWidth;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
     property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
     field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
   }
 
@@ -140,9 +135,9 @@
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.CardDefaults INSTANCE;
   }
 
@@ -177,8 +172,8 @@
   }
 
   @androidx.compose.runtime.Stable public final class ColorScheme {
-    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
-    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public long getBackground();
     method public long getError();
     method public long getErrorContainer();
@@ -197,8 +192,10 @@
     method public long getOnTertiary();
     method public long getOnTertiaryContainer();
     method public long getOutline();
+    method public long getOutlineVariant();
     method public long getPrimary();
     method public long getPrimaryContainer();
+    method public long getScrim();
     method public long getSecondary();
     method public long getSecondaryContainer();
     method public long getSurface();
@@ -224,8 +221,10 @@
     property public final long onTertiary;
     property public final long onTertiaryContainer;
     property public final long outline;
+    property public final long outlineVariant;
     property public final long primary;
     property public final long primaryContainer;
+    property public final long scrim;
     property public final long secondary;
     property public final long secondaryContainer;
     property public final long surface;
@@ -238,8 +237,8 @@
   public final class ColorSchemeKt {
     method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
-    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
-    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
   }
 
@@ -251,8 +250,8 @@
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
-    property @androidx.compose.runtime.Composable public final long Color;
     property public final float Thickness;
+    property @androidx.compose.runtime.Composable public final long color;
     field public static final androidx.compose.material3.DividerDefaults INSTANCE;
   }
 
@@ -283,12 +282,12 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
     property public final float LargeIconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
     field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
   }
 
@@ -323,8 +322,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
     field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
   }
 
@@ -384,8 +383,8 @@
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
   }
 
@@ -441,11 +440,11 @@
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
-    property @androidx.compose.runtime.Composable public final long CircularColor;
     property public final float CircularStrokeWidth;
-    property @androidx.compose.runtime.Composable public final long LinearColor;
-    property @androidx.compose.runtime.Composable public final long LinearTrackColor;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property @androidx.compose.runtime.Composable public final long circularColor;
+    property @androidx.compose.runtime.Composable public final long linearColor;
+    property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
   }
 
@@ -533,12 +532,12 @@
     method @androidx.compose.runtime.Composable public long getContentColor();
     method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ActionColor;
-    property @androidx.compose.runtime.Composable public final long ActionContentColor;
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
-    property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final long actionColor;
+    property @androidx.compose.runtime.Composable public final long actionContentColor;
+    property @androidx.compose.runtime.Composable public final long color;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
   }
 
@@ -632,11 +631,11 @@
   public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @androidx.compose.runtime.Composable public long getColor();
+    method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
     method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
     field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 94eb7e0..e9d5bf1 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -8,12 +8,12 @@
     method @androidx.compose.runtime.Composable public long getTextContentColor();
     method @androidx.compose.runtime.Composable public long getTitleContentColor();
     method public float getTonalElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long IconContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final long TextContentColor;
-    property @androidx.compose.runtime.Composable public final long TitleContentColor;
     property public final float TonalElevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long iconContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final long textContentColor;
+    property @androidx.compose.runtime.Composable public final long titleContentColor;
     field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
   }
 
@@ -50,13 +50,13 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     property public final float Height;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.AssistChipDefaults INSTANCE;
   }
 
   public final class BadgeDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
   }
 
@@ -66,26 +66,21 @@
   }
 
   public final class BottomAppBarDefaults {
-    method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float ContainerElevation;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+    property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
-  public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
-    method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+  public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
-    property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
-    field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+    field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
   }
 
   @androidx.compose.runtime.Stable public interface ButtonColors {
@@ -117,17 +112,17 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
     property public final float IconSize;
     property public final float IconSpacing;
     property public final float MinHeight;
     property public final float MinWidth;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
     property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
     field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
   }
 
@@ -160,9 +155,9 @@
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.CardDefaults INSTANCE;
   }
 
@@ -215,16 +210,16 @@
   public final class ChipKt {
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
   }
 
   @androidx.compose.runtime.Stable public final class ColorScheme {
-    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
-    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public long getBackground();
     method public long getError();
     method public long getErrorContainer();
@@ -243,8 +238,10 @@
     method public long getOnTertiary();
     method public long getOnTertiaryContainer();
     method public long getOutline();
+    method public long getOutlineVariant();
     method public long getPrimary();
     method public long getPrimaryContainer();
+    method public long getScrim();
     method public long getSecondary();
     method public long getSecondaryContainer();
     method public long getSurface();
@@ -270,8 +267,10 @@
     property public final long onTertiary;
     property public final long onTertiaryContainer;
     property public final long outline;
+    property public final long outlineVariant;
     property public final long primary;
     property public final long primaryContainer;
+    property public final long scrim;
     property public final long secondary;
     property public final long secondaryContainer;
     property public final long surface;
@@ -284,8 +283,8 @@
   public final class ColorSchemeKt {
     method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
-    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
-    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
   }
 
@@ -297,8 +296,8 @@
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
-    property @androidx.compose.runtime.Composable public final long Color;
     property public final float Thickness;
+    property @androidx.compose.runtime.Composable public final long color;
     field public static final androidx.compose.material3.DividerDefaults INSTANCE;
   }
 
@@ -316,12 +315,12 @@
     method public float getPermanentDrawerElevation();
     method @androidx.compose.runtime.Composable public long getScrimColor();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float DismissibleDrawerElevation;
     property public final float ModalDrawerElevation;
     property public final float PermanentDrawerElevation;
-    property @androidx.compose.runtime.Composable public final long ScrimColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.DrawerDefaults INSTANCE;
   }
 
@@ -406,7 +405,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     property public final float Height;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.FilterChipDefaults INSTANCE;
   }
 
@@ -419,12 +418,12 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
     property public final float LargeIconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
     field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
   }
 
@@ -459,8 +458,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
     field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
   }
 
@@ -500,7 +499,7 @@
     property public final float AvatarSize;
     property public final float Height;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.InputChipDefaults INSTANCE;
   }
 
@@ -519,10 +518,10 @@
     method @androidx.compose.runtime.Composable public long getContentColor();
     method public float getElevation();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
     property public final float Elevation;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
   }
 
@@ -564,8 +563,8 @@
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
   }
 
@@ -643,11 +642,11 @@
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
-    property @androidx.compose.runtime.Composable public final long CircularColor;
     property public final float CircularStrokeWidth;
-    property @androidx.compose.runtime.Composable public final long LinearColor;
-    property @androidx.compose.runtime.Composable public final long LinearTrackColor;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property @androidx.compose.runtime.Composable public final long circularColor;
+    property @androidx.compose.runtime.Composable public final long linearColor;
+    property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
   }
 
@@ -753,12 +752,12 @@
     method @androidx.compose.runtime.Composable public long getContentColor();
     method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ActionColor;
-    property @androidx.compose.runtime.Composable public final long ActionContentColor;
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
-    property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final long actionColor;
+    property @androidx.compose.runtime.Composable public final long actionContentColor;
+    property @androidx.compose.runtime.Composable public final long color;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
   }
 
@@ -819,7 +818,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipElevation suggestionChipElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
     property public final float Height;
     property public final float IconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.SuggestionChipDefaults INSTANCE;
   }
 
@@ -871,11 +870,11 @@
   public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @androidx.compose.runtime.Composable public long getColor();
+    method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
     method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
     field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
   }
 
@@ -913,12 +912,12 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor);
     method @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.foundation.layout.PaddingValues textFieldWithLabelPadding(optional float start, optional float end, optional float top, optional float bottom);
     method @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.foundation.layout.PaddingValues textFieldWithoutLabelPadding(optional float start, optional float top, optional float end, optional float bottom);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
     property public final float FocusedBorderThickness;
     property public final float MinHeight;
     property public final float MinWidth;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
     property public final float UnfocusedBorderThickness;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
     field public static final androidx.compose.material3.TextFieldDefaults INSTANCE;
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 7a57b3e..88e4217 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -8,12 +8,12 @@
     method @androidx.compose.runtime.Composable public long getTextContentColor();
     method @androidx.compose.runtime.Composable public long getTitleContentColor();
     method public float getTonalElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long IconContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final long TextContentColor;
-    property @androidx.compose.runtime.Composable public final long TitleContentColor;
     property public final float TonalElevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long iconContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final long textContentColor;
+    property @androidx.compose.runtime.Composable public final long titleContentColor;
     field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
   }
 
@@ -36,7 +36,7 @@
 
   public final class BadgeDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
   }
 
@@ -46,26 +46,21 @@
   }
 
   public final class BottomAppBarDefaults {
-    method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float ContainerElevation;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+    property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
-  public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
-    method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+  public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
-    property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
-    field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+    field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
   }
 
   @androidx.compose.runtime.Stable public interface ButtonColors {
@@ -97,17 +92,17 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
     property public final float IconSize;
     property public final float IconSpacing;
     property public final float MinHeight;
     property public final float MinWidth;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
     property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
     field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
   }
 
@@ -140,9 +135,9 @@
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.CardDefaults INSTANCE;
   }
 
@@ -177,8 +172,8 @@
   }
 
   @androidx.compose.runtime.Stable public final class ColorScheme {
-    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
-    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+    method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public long getBackground();
     method public long getError();
     method public long getErrorContainer();
@@ -197,8 +192,10 @@
     method public long getOnTertiary();
     method public long getOnTertiaryContainer();
     method public long getOutline();
+    method public long getOutlineVariant();
     method public long getPrimary();
     method public long getPrimaryContainer();
+    method public long getScrim();
     method public long getSecondary();
     method public long getSecondaryContainer();
     method public long getSurface();
@@ -224,8 +221,10 @@
     property public final long onTertiary;
     property public final long onTertiaryContainer;
     property public final long outline;
+    property public final long outlineVariant;
     property public final long primary;
     property public final long primaryContainer;
+    property public final long scrim;
     property public final long secondary;
     property public final long secondaryContainer;
     property public final long surface;
@@ -238,8 +237,8 @@
   public final class ColorSchemeKt {
     method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
-    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
-    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+    method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
     method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
   }
 
@@ -251,8 +250,8 @@
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
-    property @androidx.compose.runtime.Composable public final long Color;
     property public final float Thickness;
+    property @androidx.compose.runtime.Composable public final long color;
     field public static final androidx.compose.material3.DividerDefaults INSTANCE;
   }
 
@@ -283,12 +282,12 @@
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
     property public final float LargeIconSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
     field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
   }
 
@@ -323,8 +322,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
     field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
   }
 
@@ -384,8 +383,8 @@
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
     property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
     field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
   }
 
@@ -441,11 +440,11 @@
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
-    property @androidx.compose.runtime.Composable public final long CircularColor;
     property public final float CircularStrokeWidth;
-    property @androidx.compose.runtime.Composable public final long LinearColor;
-    property @androidx.compose.runtime.Composable public final long LinearTrackColor;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property @androidx.compose.runtime.Composable public final long circularColor;
+    property @androidx.compose.runtime.Composable public final long linearColor;
+    property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
   }
 
@@ -533,12 +532,12 @@
     method @androidx.compose.runtime.Composable public long getContentColor();
     method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ActionColor;
-    property @androidx.compose.runtime.Composable public final long ActionContentColor;
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
-    property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+    property @androidx.compose.runtime.Composable public final long actionColor;
+    property @androidx.compose.runtime.Composable public final long actionContentColor;
+    property @androidx.compose.runtime.Composable public final long color;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
   }
 
@@ -632,11 +631,11 @@
   public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @androidx.compose.runtime.Composable public long getColor();
+    method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
     method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
-    property @androidx.compose.runtime.Composable public final long Color;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
     field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
   }
 
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index f756603..c8eb38d 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -31,6 +31,7 @@
 import androidx.compose.material3.samples.ButtonWithIconSample
 import androidx.compose.material3.samples.CardSample
 import androidx.compose.material3.samples.CheckboxSample
+import androidx.compose.material3.samples.CheckboxWithTextSample
 import androidx.compose.material3.samples.ChipGroupSingleLineSample
 import androidx.compose.material3.samples.CircularProgressIndicatorSample
 import androidx.compose.material3.samples.ClickableCardSample
@@ -237,6 +238,13 @@
         CheckboxSample()
     },
     Example(
+        name = ::CheckboxWithTextSample.name,
+        description = CheckboxesExampleDescription,
+        sourceUrl = CheckboxesExampleSourceUrl
+    ) {
+        CheckboxWithTextSample()
+    },
+    Example(
         name = ::TriStateCheckboxSample.name,
         description = CheckboxesExampleDescription,
         sourceUrl = CheckboxesExampleSourceUrl
@@ -886,6 +894,7 @@
         Box(
             Modifier
                 .wrapContentWidth()
-                .width(280.dp)) { it.content() }
+                .width(280.dp)
+        ) { it.content() }
     })
 }
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
index 9dbadaf..8df1367 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
@@ -139,9 +139,19 @@
                  Error Container")
             Spacer(modifier = Modifier.height(16.dp))
             Text("Utility", style = MaterialTheme.typography.bodyLarge)
-            ColorTile(
-                text = "Outline",
-                color = colorScheme.outline,
+            DoubleTile(
+                leftTile = {
+                    ColorTile(
+                        text = "Outline",
+                        color = colorScheme.outline,
+                    )
+                },
+                rightTile = {
+                    ColorTile(
+                        text = "Outline Variant",
+                        color = colorScheme.outlineVariant,
+                    )
+                }
             )
         }
     }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 3ba030d..8cfa713 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -32,6 +32,7 @@
 import androidx.compose.material3.BottomAppBarDefaults
 import androidx.compose.material3.CenterAlignedTopAppBar
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.LargeTopAppBar
@@ -404,10 +405,12 @@
             }
         },
         floatingActionButton = {
-            BottomAppBarDefaults.FloatingActionButton(
+            FloatingActionButton(
                  /* do something */ },
+                containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                elevation = BottomAppBarDefaults.BottomAppBarFabElevation
             ) {
-                Icon(Icons.Filled.Add, contentDescription = "Localized description")
+                Icon(Icons.Filled.Add, "Localized description")
             }
         }
     )
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
index b363467..b057f4d 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
@@ -18,15 +18,23 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.toggleable
 import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.material3.TriStateCheckbox
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.unit.dp
 
@@ -42,6 +50,34 @@
 
 @Sampled
 @Composable
+fun CheckboxWithTextSample() {
+    val (checkedState, onStateChange) = remember { mutableStateOf(true) }
+    Row(
+        Modifier
+            .fillMaxWidth()
+            .height(56.dp)
+            .toggleable(
+                value = checkedState,
+                 onStateChange(!checkedState) },
+                role = Role.Checkbox
+            )
+            .padding(horizontal = 16.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        Checkbox(
+            checked = checkedState,
+             // null recommended for accessibility with screenreaders
+        )
+        Text(
+            text = "Option selection",
+            style = MaterialTheme.typography.bodyLarge,
+            modifier = Modifier.padding(start = 16.dp)
+        )
+    }
+}
+
+@Sampled
+@Composable
 fun TriStateCheckboxSample() {
     Column {
         // define dependent checkboxes states
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
index 22c5d76..2837fd9 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
@@ -93,12 +93,16 @@
         selected = selected,
          selected = !selected },
         label = { Text("Filter chip") },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            null
         }
     )
 }
@@ -112,12 +116,16 @@
         selected = selected,
          selected = !selected },
         label = { Text("Filter chip") },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            null
         }
     )
 }
@@ -131,19 +139,22 @@
         selected = selected,
          selected = !selected },
         label = { Text("Filter chip") },
-        leadingIcon = {
-            Icon(
-                imageVector = Icons.Filled.Home,
-                contentDescription = "Localized description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
-        },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Home,
+                    contentDescription = "Localized description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
         }
     )
 }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
index 82b81d6..b0af8fd 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
@@ -35,6 +35,8 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -42,15 +44,20 @@
 fun RadioButtonSample() {
     // We have two radio buttons and only one can be selected
     var state by remember { mutableStateOf(true) }
-    // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior
+    // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior.
+    // We also set a content description for this sample, but note that a RadioButton would usually
+    // be part of a higher level component, such as a raw with text, and that component would need
+    // to provide an appropriate content description. See RadioGroupSample.
     Row(Modifier.selectableGroup()) {
         RadioButton(
             selected = state,
-             state = true }
+             state = true },
+            modifier = Modifier.semantics { contentDescription = "Localized Description" }
         )
         RadioButton(
             selected = !state,
-             state = false }
+             state = false },
+            modifier = Modifier.semantics { contentDescription = "Localized Description" }
         )
     }
 }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index 0b29639..10c2353 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -312,8 +312,10 @@
                         }
                     },
                     floatingActionButton = {
-                        BottomAppBarDefaults.FloatingActionButton(
-                             /* do something */ }
+                        FloatingActionButton(
+                             /* do something */ },
+                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            elevation = BottomAppBarDefaults.BottomAppBarFabElevation
                         ) {
                             Icon(Icons.Filled.Add, "Localized description")
                         }
@@ -342,8 +344,10 @@
                         }
                     },
                     floatingActionButton = {
-                        BottomAppBarDefaults.FloatingActionButton(
-                             /* do something */ }
+                        FloatingActionButton(
+                             /* do something */ },
+                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            elevation = BottomAppBarDefaults.BottomAppBarFabElevation
                         ) {
                             Icon(Icons.Filled.Add, "Localized description")
                         }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index a9c1692..a83167cb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -814,8 +814,10 @@
                 BottomAppBar(
                     icons = {},
                     floatingActionButton = {
-                        BottomAppBarDefaults.FloatingActionButton(
-                             /* do something */ }
+                        FloatingActionButton(
+                             /* do something */ },
+                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            elevation = BottomAppBarDefaults.BottomAppBarFabElevation
                         ) {
                             Icon(Icons.Filled.Add, "Localized description")
                         }
@@ -863,9 +865,11 @@
                 icons = {},
                 Modifier.testTag("bar"),
                 floatingActionButton = {
-                    BottomAppBarDefaults.FloatingActionButton(
-                        modifier = Modifier.testTag("FAB"),
+                    FloatingActionButton(
                          /* do something */ },
+                        modifier = Modifier.testTag("FAB"),
+                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                        elevation = BottomAppBarDefaults.BottomAppBarFabElevation
                     ) {
                         Icon(Icons.Filled.Add, "Localized description")
                     }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
index e1d448a..2df1372 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
@@ -20,7 +20,6 @@
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Done
-import androidx.compose.material.icons.filled.Home
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.testutils.assertAgainstGolden
@@ -329,7 +328,7 @@
                 >
                 label = { Text("Filter Chip") },
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
@@ -342,58 +341,6 @@
     }
 
     @Test
-    fun filterChip_flat_withLeadingIcon_selected_lightTheme() {
-        rule.setMaterialContent(lightColorScheme()) {
-            FilterChip(
-                selected = true,
-                >
-                label = { Text("Filter Chip") },
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_lightTheme")
-    }
-
-    @Test
-    fun filterChip_flat_withLeadingIcon_selected_darkTheme() {
-        rule.setMaterialContent(darkColorScheme()) {
-            FilterChip(
-                selected = true,
-                >
-                label = { Text("Filter Chip") },
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_darkTheme")
-    }
-
-    @Test
     fun filterChip_flat_notSelected() {
         rule.setMaterialContent(lightColorScheme()) {
             FilterChip(
@@ -415,7 +362,7 @@
                 label = { Text("Filter Chip") },
                 enabled = false,
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         tint = LocalContentColor.current,
@@ -429,33 +376,6 @@
     }
 
     @Test
-    fun filterChip_flat_withLeadingIcon_disabled_selected() {
-        rule.setMaterialContent(lightColorScheme()) {
-            FilterChip(
-                selected = true,
-                >
-                label = { Text("Filter Chip") },
-                enabled = false,
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_disabled_selected")
-    }
-
-    @Test
     fun filterChip_flat_disabled_notSelected() {
         rule.setMaterialContent(lightColorScheme()) {
             FilterChip(
@@ -477,7 +397,7 @@
                 >
                 label = { Text("Filter Chip") },
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
index 1a173e3..2dd3963 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
@@ -360,13 +360,6 @@
                         "Filter chip",
                         Modifier.testTag(TestChipTag)
                     )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.size(FilterChipDefaults.IconSize)
-                    )
                 })
         }
 
@@ -395,7 +388,7 @@
                         Modifier.testTag(TestChipTag)
                     )
                 },
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
index 8a7c1bf..3d80b1c 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
@@ -188,7 +188,7 @@
             .performTouchInput { move(); up() }
 
         rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(milliseconds = 96)
+        rule.mainClock.advanceTimeBy(milliseconds = 100)
 
         // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
         // synchronization. Instead just wait until after the ripples are finished animating.
@@ -217,7 +217,7 @@
             .performTouchInput { move(); up() }
 
         rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(milliseconds = 96)
+        rule.mainClock.advanceTimeBy(milliseconds = 100)
 
         // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
         // synchronization. Instead just wait until after the ripples are finished animating.
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
index be98b5c..9eb0f3d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
@@ -296,6 +296,27 @@
             .assertLeftPositionInRootIsEqualTo(8.dp)
     }
 
+    @Test
+    fun switch_constantState_doesNotAnimate() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val spacer = @Composable { Spacer(Modifier.size(16.dp).testTag("spacer")) }
+            Switch(
+                modifier = Modifier.testTag(defaultSwitchTag),
+                checked = false,
+                thumbContent = spacer,
+                >
+            )
+        }
+
+        rule.onNodeWithTag(defaultSwitchTag)
+            .performTouchInput {
+                click(center)
+            }
+
+        rule.onNodeWithTag("spacer", useUnmergedTree = true)
+            .assertLeftPositionInRootIsEqualTo(8.dp)
+    }
+
     // regression test for b/191375128
     @Test
     fun switch_stateRestoration_stateChangeWhileSaved() {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
index 8469858..cd77885 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
@@ -77,12 +77,12 @@
     icon: @Composable (() -> Unit)? = null,
     title: @Composable (() -> Unit)? = null,
     text: @Composable (() -> Unit)? = null,
-    shape: Shape = AlertDialogDefaults.Shape,
-    containerColor: Color = AlertDialogDefaults.ContainerColor,
+    shape: Shape = AlertDialogDefaults.shape,
+    containerColor: Color = AlertDialogDefaults.containerColor,
     tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
-    iconContentColor: Color = AlertDialogDefaults.IconContentColor,
-    titleContentColor: Color = AlertDialogDefaults.TitleContentColor,
-    textContentColor: Color = AlertDialogDefaults.TextContentColor,
+    iconContentColor: Color = AlertDialogDefaults.iconContentColor,
+    titleContentColor: Color = AlertDialogDefaults.titleContentColor,
+    textContentColor: Color = AlertDialogDefaults.textContentColor,
     properties: DialogProperties = DialogProperties()
 ) {
     Dialog(
@@ -123,22 +123,22 @@
  */
 object AlertDialogDefaults {
     /** The default shape for alert dialogs */
-    val Shape: Shape @Composable get() = DialogTokens.ContainerShape.toShape()
+    val shape: Shape @Composable get() = DialogTokens.ContainerShape.toShape()
 
     /** The default container color for alert dialogs */
-    val ContainerColor: Color @Composable get() = DialogTokens.ContainerColor.toColor()
+    val containerColor: Color @Composable get() = DialogTokens.ContainerColor.toColor()
+
+    /** The default icon color for alert dialogs */
+    val iconContentColor: Color @Composable get() = DialogTokens.IconColor.toColor()
+
+    /** The default title color for alert dialogs */
+    val titleContentColor: Color @Composable get() = DialogTokens.SubheadColor.toColor()
+
+    /** The default text color for alert dialogs */
+    val textContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.toColor()
 
     /** The default tonal elevation for alert dialogs */
     val TonalElevation: Dp = DialogTokens.ContainerElevation
-
-    /** The default icon color for alert dialogs */
-    val IconContentColor: Color @Composable get() = DialogTokens.IconColor.toColor()
-
-    /** The default title color for alert dialogs */
-    val TitleContentColor: Color @Composable get() = DialogTokens.SubheadColor.toColor()
-
-    /** The default text color for alert dialogs */
-    val TextContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.toColor()
 }
 
 private val ButtonsMainAxisSpacing = 8.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index 55d4be4..efc3e9d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -27,9 +27,7 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
-import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -41,7 +39,6 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.BottomAppBarDefaults.FloatingActionButton
 import androidx.compose.material3.tokens.BottomAppBarTokens
 import androidx.compose.material3.tokens.FabSecondaryTokens
 import androidx.compose.material3.tokens.TopAppBarLargeTokens
@@ -66,7 +63,6 @@
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -331,7 +327,7 @@
     icons: @Composable RowScope.() -> Unit,
     modifier: Modifier = Modifier,
     floatingActionButton: @Composable (() -> Unit)? = null,
-    containerColor: Color = BottomAppBarDefaults.ContainerColor,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
@@ -386,7 +382,7 @@
 @Composable
 fun BottomAppBar(
     modifier: Modifier = Modifier,
-    containerColor: Color = BottomAppBarDefaults.ContainerColor,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
@@ -863,7 +859,7 @@
 object BottomAppBarDefaults {
 
     /** Default color used for [BottomAppBar] container **/
-    val ContainerColor: Color @Composable get() = BottomAppBarTokens.ContainerColor.toColor()
+    val containerColor: Color @Composable get() = BottomAppBarTokens.ContainerColor.toColor()
 
     /** Default elevation used for [BottomAppBar] **/
     val ContainerElevation: Dp = BottomAppBarTokens.ContainerElevation
@@ -883,9 +879,8 @@
      * Creates a [FloatingActionButtonElevation] that represents the default elevation of a
      * [FloatingActionButton] used for [BottomAppBar] in different states.
      */
-    object FloatingActionButtonElevation :
-        androidx.compose.material3.FloatingActionButtonElevation {
-        val elevation = mutableStateOf(0.dp)
+    object BottomAppBarFabElevation : FloatingActionButtonElevation {
+        private val elevation = mutableStateOf(0.dp)
 
         @Composable
         override fun shadowElevation(interactionSource: InteractionSource) = elevation
@@ -895,61 +890,9 @@
     }
 
     /** The color of a [BottomAppBar]'s [FloatingActionButton] */
-    val FloatingActionButtonContainerColor: Color
+    val bottomAppBarFabColor: Color
         @Composable get() =
             FabSecondaryTokens.ContainerColor.toColor()
-
-    /** The shape of a [BottomAppBar]'s [FloatingActionButton] */
-    val FloatingActionButtonShape: Shape
-        @Composable get() =
-            FabSecondaryTokens.ContainerShape.toShape()
-
-    /**
-     * The default [FloatingActionButton] for [BottomAppBar]
-     *
-     * A [BottomAppBar]'s FAB follows a secondary color style, as well as an elevation of zero.
-     *
-     * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
-     *
-     * @param onClick callback invoked when this FAB is clicked
-     * @param modifier [Modifier] to be applied to this FAB.
-     * @param interactionSource the [MutableInteractionSource] representing the stream of
-     * [Interaction]s for this FAB. You can create and pass in your own `remember`ed instance to
-     * observe [Interaction]s and customize the appearance / behavior of this FAB in different
-     * states.
-     * @param shape defines the shape of this FAB's container and shadow (when using [elevation])
-     * @param containerColor the color used for the background of this FAB. Use [Color.Transparent]
-     * to have no color.
-     * @param contentColor the preferred color for content inside this FAB. Defaults to either the
-     * matching content color for [containerColor], or to the current [LocalContentColor] if
-     * [containerColor] is not a color from the theme.
-     * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB
-     * in different states. This controls the size of the shadow below the FAB. Additionally, when
-     * the container color is [ColorScheme.surface], this controls the amount of primary color
-     * applied as an overlay. See also: [Surface].
-     * @param content the content of this FAB - this is typically an [Icon].
-     */
-    @Composable
-    fun FloatingActionButton(
-        onClick: () -> Unit,
-        modifier: Modifier = Modifier,
-        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-        shape: Shape = FloatingActionButtonShape,
-        containerColor: Color = FloatingActionButtonContainerColor,
-        contentColor: Color = contentColorFor(containerColor),
-        elevation: androidx.compose.material3.FloatingActionButtonElevation =
-            FloatingActionButtonElevation,
-        content: @Composable () -> Unit,
-    ) = androidx.compose.material3.FloatingActionButton(
-        >
-        modifier = modifier,
-        interactionSource = interactionSource,
-        shape = shape,
-        containerColor = containerColor,
-        contentColor = contentColor,
-        elevation = elevation,
-        content = content
-    )
 }
 
 // Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
index a19294f..8fe2b12b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
@@ -137,7 +137,7 @@
 @Composable
 fun Badge(
     modifier: Modifier = Modifier,
-    containerColor: Color = BadgeDefaults.ContainerColor,
+    containerColor: Color = BadgeDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     content: @Composable (RowScope.() -> Unit)? = null,
 ) {
@@ -185,7 +185,7 @@
 /** Default values used for [Badge] implementations. */
 object BadgeDefaults {
     /** Default container color for a badge. */
-    val ContainerColor: Color @Composable get() = BadgeTokens.Color.toColor()
+    val containerColor: Color @Composable get() = BadgeTokens.Color.toColor()
 }
 
 /*@VisibleForTesting*/
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index a0b2199..6eaa263 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -106,7 +106,7 @@
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
-    shape: Shape = ButtonDefaults.Shape,
+    shape: Shape = ButtonDefaults.shape,
     border: BorderStroke? = null,
     colors: ButtonColors = ButtonDefaults.buttonColors(),
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -199,7 +199,7 @@
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
-    shape: Shape = ButtonDefaults.ElevatedShape,
+    shape: Shape = ButtonDefaults.elevatedShape,
     border: BorderStroke? = null,
     colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -269,7 +269,7 @@
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
-    shape: Shape = ButtonDefaults.FilledTonalShape,
+    shape: Shape = ButtonDefaults.filledTonalShape,
     border: BorderStroke? = null,
     colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -338,7 +338,7 @@
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ButtonElevation? = null,
-    shape: Shape = ButtonDefaults.OutlinedShape,
+    shape: Shape = ButtonDefaults.outlinedShape,
     border: BorderStroke? = ButtonDefaults.outlinedButtonBorder,
     colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -409,7 +409,7 @@
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ButtonElevation? = null,
-    shape: Shape = ButtonDefaults.TextShape,
+    shape: Shape = ButtonDefaults.textShape,
     border: BorderStroke? = null,
     colors: ButtonColors = ButtonDefaults.textButtonColors(),
     contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
@@ -518,21 +518,20 @@
     // TODO(b/201344013): Make sure this value stays up to date until replaced with a token.
     val IconSpacing = 8.dp
 
-    // Shape Defaults
     /** Default shape for a button. */
-    val Shape: Shape @Composable get() = FilledButtonTokens.ContainerShape.toShape()
+    val shape: Shape @Composable get() = FilledButtonTokens.ContainerShape.toShape()
 
     /** Default shape for an elevated button. */
-    val ElevatedShape: Shape @Composable get() = ElevatedButtonTokens.ContainerShape.toShape()
+    val elevatedShape: Shape @Composable get() = ElevatedButtonTokens.ContainerShape.toShape()
 
     /** Default shape for a filled tonal button. */
-    val FilledTonalShape: Shape @Composable get() = FilledTonalButtonTokens.ContainerShape.toShape()
+    val filledTonalShape: Shape @Composable get() = FilledTonalButtonTokens.ContainerShape.toShape()
 
     /** Default shape for an outlined button. */
-    val OutlinedShape: Shape @Composable get() = OutlinedButtonTokens.ContainerShape.toShape()
+    val outlinedShape: Shape @Composable get() = OutlinedButtonTokens.ContainerShape.toShape()
 
     /** Default shape for a text button. */
-    val TextShape: Shape @Composable get() = TextButtonTokens.ContainerShape.toShape()
+    val textShape: Shape @Composable get() = TextButtonTokens.ContainerShape.toShape()
 
     /**
      * Creates a [ButtonColors] that represents the default container and content colors used in a
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
index 398117a..cedd229 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
@@ -46,7 +46,6 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.unit.Dp
-import kotlinx.coroutines.flow.collect
 
 /**
  * <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design filled card</a>.
@@ -77,7 +76,7 @@
 @Composable
 fun Card(
     modifier: Modifier = Modifier,
-    shape: Shape = CardDefaults.Shape,
+    shape: Shape = CardDefaults.shape,
     border: BorderStroke? = null,
     elevation: CardElevation = CardDefaults.cardElevation(),
     colors: CardColors = CardDefaults.cardColors(),
@@ -135,7 +134,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = CardDefaults.Shape,
+    shape: Shape = CardDefaults.shape,
     border: BorderStroke? = null,
     elevation: CardElevation = CardDefaults.cardElevation(),
     colors: CardColors = CardDefaults.cardColors(),
@@ -184,7 +183,7 @@
 @Composable
 fun ElevatedCard(
     modifier: Modifier = Modifier,
-    shape: Shape = CardDefaults.ElevatedShape,
+    shape: Shape = CardDefaults.elevatedShape,
     elevation: CardElevation = CardDefaults.elevatedCardElevation(),
     colors: CardColors = CardDefaults.elevatedCardColors(),
     content: @Composable ColumnScope.() -> Unit
@@ -234,7 +233,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = CardDefaults.ElevatedShape,
+    shape: Shape = CardDefaults.elevatedShape,
     elevation: CardElevation = CardDefaults.elevatedCardElevation(),
     colors: CardColors = CardDefaults.elevatedCardColors(),
     content: @Composable ColumnScope.() -> Unit
@@ -278,7 +277,7 @@
 @Composable
 fun OutlinedCard(
     modifier: Modifier = Modifier,
-    shape: Shape = CardDefaults.OutlinedShape,
+    shape: Shape = CardDefaults.outlinedShape,
     border: BorderStroke = CardDefaults.outlinedCardBorder(),
     elevation: CardElevation = CardDefaults.outlinedCardElevation(),
     colors: CardColors = CardDefaults.outlinedCardColors(),
@@ -330,7 +329,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = CardDefaults.OutlinedShape,
+    shape: Shape = CardDefaults.outlinedShape,
     border: BorderStroke = CardDefaults.outlinedCardBorder(enabled),
     elevation: CardElevation = CardDefaults.outlinedCardElevation(),
     colors: CardColors = CardDefaults.outlinedCardColors(),
@@ -417,15 +416,15 @@
  * Contains the default values used by all card types.
  */
 object CardDefaults {
-    // Shape Defaults
+    // shape Defaults
     /** Default shape for a card. */
-    val Shape: Shape @Composable get() = FilledCardTokens.ContainerShape.toShape()
+    val shape: Shape @Composable get() = FilledCardTokens.ContainerShape.toShape()
 
     /** Default shape for an elevated card. */
-    val ElevatedShape: Shape @Composable get() = ElevatedCardTokens.ContainerShape.toShape()
+    val elevatedShape: Shape @Composable get() = ElevatedCardTokens.ContainerShape.toShape()
 
     /** Default shape for an outlined card. */
-    val OutlinedShape: Shape @Composable get() = OutlinedCardTokens.ContainerShape.toShape()
+    val outlinedShape: Shape @Composable get() = OutlinedCardTokens.ContainerShape.toShape()
 
     /**
      * Creates a [CardElevation] that will animate between the provided values according to the
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
index fed31c8..d8483ff 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
@@ -64,8 +64,12 @@
  *
  * ![Checkbox image](https://developer.android.com/images/reference/androidx/compose/material3/checkbox.png)
  *
+ * Simple Checkbox sample:
  * @sample androidx.compose.material3.samples.CheckboxSample
  *
+ * Combined Checkbox with Text sample:
+ * @sample androidx.compose.material3.samples.CheckboxWithTextSample
+ *
  * @see [TriStateCheckbox] if you require support for an indeterminate state.
  *
  * @param checked whether this checkbox is checked or unchecked
@@ -313,7 +317,10 @@
     val checkColor = colors.checkmarkColor(value)
     val boxColor = colors.boxColor(enabled, value)
     val borderColor = colors.borderColor(enabled, value)
-    Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(CheckboxSize)) {
+    Canvas(
+        modifier
+            .wrapContentSize(Alignment.Center)
+            .requiredSize(CheckboxSize)) {
         val strokeWidthPx = floor(StrokeWidth.toPx())
         drawBox(
             boxColor = boxColor.value,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 8f46b4a..71ae1a7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -112,7 +112,7 @@
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ChipElevation? = AssistChipDefaults.assistChipElevation(),
-    shape: Shape = AssistChipDefaults.Shape,
+    shape: Shape = AssistChipDefaults.shape,
     border: ChipBorder? = AssistChipDefaults.assistChipBorder(),
     colors: ChipColors = AssistChipDefaults.assistChipColors()
 ) = Chip(
@@ -184,7 +184,7 @@
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ChipElevation? = AssistChipDefaults.elevatedAssistChipElevation(),
-    shape: Shape = AssistChipDefaults.Shape,
+    shape: Shape = AssistChipDefaults.shape,
     border: ChipBorder? = null,
     colors: ChipColors = AssistChipDefaults.elevatedAssistChipColors()
 ) = Chip(
@@ -220,8 +220,8 @@
  * This filter chip is applied with a flat style. If you want an elevated style, use the
  * [ElevatedFilterChip].
  *
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
  *
  * Example of a flat FilterChip with a trailing icon:
  * @sample androidx.compose.material3.samples.FilterChipSample
@@ -236,9 +236,9 @@
  * @param enabled controls the enabled state of this chip. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
  * @param trailingIcon optional icon at the end of the chip
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -263,11 +263,10 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     leadingIcon: @Composable (() -> Unit)? = null,
-    selectedIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
-    shape: Shape = FilterChipDefaults.Shape,
+    shape: Shape = FilterChipDefaults.shape,
     border: SelectableChipBorder? = FilterChipDefaults.filterChipBorder(),
     colors: SelectableChipColors = FilterChipDefaults.filterChipColors()
 ) = SelectableChip(
@@ -277,7 +276,7 @@
     enabled = enabled,
     label = label,
     labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
-    leadingIcon = if (selected) selectedIcon else leadingIcon,
+    leadingIcon = leadingIcon,
     avatar = null,
     trailingIcon = trailingIcon,
     elevation = elevation,
@@ -304,8 +303,8 @@
  * This filter chip is applied with an elevated style. If you want a flat style, use the
  * [FilterChip].
  *
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
  *
  * Example of an elevated FilterChip with a trailing icon:
  * @sample androidx.compose.material3.samples.ElevatedFilterChipSample
@@ -317,9 +316,9 @@
  * @param enabled controls the enabled state of this chip. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
  * @param trailingIcon optional icon at the end of the chip
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -344,11 +343,10 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     leadingIcon: @Composable (() -> Unit)? = null,
-    selectedIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: SelectableChipElevation? = FilterChipDefaults.elevatedFilterChipElevation(),
-    shape: Shape = FilterChipDefaults.Shape,
+    shape: Shape = FilterChipDefaults.shape,
     border: SelectableChipBorder? = null,
     colors: SelectableChipColors = FilterChipDefaults.elevatedFilterChipColors()
 ) = SelectableChip(
@@ -358,7 +356,7 @@
     enabled = enabled,
     label = label,
     labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
-    leadingIcon = if (selected) selectedIcon else leadingIcon,
+    leadingIcon = leadingIcon,
     avatar = null,
     trailingIcon = trailingIcon,
     elevation = elevation,
@@ -433,7 +431,7 @@
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: SelectableChipElevation? = InputChipDefaults.inputChipElevation(),
-    shape: Shape = InputChipDefaults.Shape,
+    shape: Shape = InputChipDefaults.shape,
     border: SelectableChipBorder? = InputChipDefaults.inputChipBorder(),
     colors: SelectableChipColors = InputChipDefaults.inputChipColors()
 ) {
@@ -529,7 +527,7 @@
     icon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ChipElevation? = SuggestionChipDefaults.suggestionChipElevation(),
-    shape: Shape = SuggestionChipDefaults.Shape,
+    shape: Shape = SuggestionChipDefaults.shape,
     border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(),
     colors: ChipColors = SuggestionChipDefaults.suggestionChipColors()
 ) = Chip(
@@ -598,7 +596,7 @@
     icon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: ChipElevation? = SuggestionChipDefaults.elevatedSuggestionChipElevation(),
-    shape: Shape = SuggestionChipDefaults.Shape,
+    shape: Shape = SuggestionChipDefaults.shape,
     border: ChipBorder? = null,
     colors: ChipColors = SuggestionChipDefaults.elevatedSuggestionChipColors()
 ) = Chip(
@@ -829,9 +827,6 @@
  */
 @ExperimentalMaterial3Api
 object AssistChipDefaults {
-    /** Default shape of an assist chip. */
-    val Shape: Shape @Composable get() = AssistChipTokens.ContainerShape.toShape()
-
     /**
      * The height applied for an assist chip.
      * Note that you can override it by applying Modifier.height directly on a chip.
@@ -1024,6 +1019,9 @@
             )
         }
     }
+
+    /** Default shape of an assist chip. */
+    val shape: Shape @Composable get() = AssistChipTokens.ContainerShape.toShape()
 }
 
 /**
@@ -1031,9 +1029,6 @@
  */
 @ExperimentalMaterial3Api
 object FilterChipDefaults {
-    /** Default shape of a filter chip. */
-    val Shape: Shape @Composable get() = FilterChipTokens.ContainerShape.toShape()
-
     /**
      * The height applied for a filter chip.
      * Note that you can override it by applying Modifier.height directly on a chip.
@@ -1269,6 +1264,9 @@
             )
         }
     }
+
+    /** Default shape of a filter chip. */
+    val shape: Shape @Composable get() = FilterChipTokens.ContainerShape.toShape()
 }
 
 /**
@@ -1276,9 +1274,6 @@
  */
 @ExperimentalMaterial3Api
 object InputChipDefaults {
-    /** Default shape of an input chip. */
-    val Shape: Shape @Composable get() = InputChipTokens.ContainerShape.toShape()
-
     /**
      * The height applied for an input chip.
      * Note that you can override it by applying Modifier.height directly on a chip.
@@ -1430,6 +1425,9 @@
             )
         }
     }
+
+    /** Default shape of an input chip. */
+    val shape: Shape @Composable get() = InputChipTokens.ContainerShape.toShape()
 }
 
 /**
@@ -1437,9 +1435,6 @@
  */
 @ExperimentalMaterial3Api
 object SuggestionChipDefaults {
-    /** Default shape of a suggestion chip. */
-    val Shape: Shape @Composable get() = SuggestionChipTokens.ContainerShape.toShape()
-
     /**
      * The height applied for a suggestion chip.
      * Note that you can override it by applying Modifier.height directly on a chip.
@@ -1626,6 +1621,9 @@
             )
         }
     }
+
+    /** Default shape of a suggestion chip. */
+    val shape: Shape @Composable get() = SuggestionChipTokens.ContainerShape.toShape()
 }
 
 @ExperimentalMaterial3Api
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index 4c9c7a6..f43dc91 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -92,6 +92,9 @@
  * top of [errorContainer].
  * @property outline Subtle color used for boundaries. Outline color role adds contrast for
  * accessibility purposes.
+ * @property outlineVariant Utility color used for boundaries for decorative elements when strong
+ * contrast is not required.
+ * @property scrim Color of a scrim that obscures content.
  */
 @Stable
 class ColorScheme(
@@ -122,6 +125,8 @@
     errorContainer: Color,
     onErrorContainer: Color,
     outline: Color,
+    outlineVariant: Color,
+    scrim: Color,
 ) {
     var primary by mutableStateOf(primary, structuralEqualityPolicy())
         internal set
@@ -177,6 +182,10 @@
         internal set
     var outline by mutableStateOf(outline, structuralEqualityPolicy())
         internal set
+    var outlineVariant by mutableStateOf(outlineVariant, structuralEqualityPolicy())
+        internal set
+    var scrim by mutableStateOf(scrim, structuralEqualityPolicy())
+        internal set
 
     /** Returns a copy of this ColorScheme, optionally overriding some of the values. */
     fun copy(
@@ -207,6 +216,8 @@
         errorContainer: Color = this.errorContainer,
         onErrorContainer: Color = this.onErrorContainer,
         outline: Color = this.outline,
+        outlineVariant: Color = this.outlineVariant,
+        scrim: Color = this.scrim,
     ): ColorScheme =
         ColorScheme(
             primary = primary,
@@ -236,6 +247,8 @@
             errorContainer = errorContainer,
             >
             outline = outline,
+            outlineVariant = outlineVariant,
+            scrim = scrim,
         )
 
     override fun toString(): String {
@@ -267,6 +280,8 @@
             "errorContainer=$errorContainer" +
             " +
             "outline=$outline" +
+            "outlineVariant=$outlineVariant" +
+            "scrim=$scrim" +
             ")"
     }
 }
@@ -302,6 +317,8 @@
     errorContainer: Color = ColorLightTokens.ErrorContainer,
     onErrorContainer: Color = ColorLightTokens.OnErrorContainer,
     outline: Color = ColorLightTokens.Outline,
+    outlineVariant: Color = ColorLightTokens.OutlineVariant,
+    scrim: Color = ColorLightTokens.Scrim,
 ): ColorScheme =
     ColorScheme(
         primary = primary,
@@ -331,6 +348,8 @@
         errorContainer = errorContainer,
         >
         outline = outline,
+        outlineVariant = outlineVariant,
+        scrim = scrim,
     )
 
 /**
@@ -364,6 +383,8 @@
     errorContainer: Color = ColorDarkTokens.ErrorContainer,
     onErrorContainer: Color = ColorDarkTokens.OnErrorContainer,
     outline: Color = ColorDarkTokens.Outline,
+    outlineVariant: Color = ColorDarkTokens.OutlineVariant,
+    scrim: Color = ColorDarkTokens.Scrim,
 ): ColorScheme =
     ColorScheme(
         primary = primary,
@@ -393,6 +414,8 @@
         errorContainer = errorContainer,
         >
         outline = outline,
+        outlineVariant = outlineVariant,
+        scrim = scrim,
     )
 
 /**
@@ -525,6 +548,8 @@
     errorContainer = other.errorContainer
     >
     outline = other.outline
+    outlineVariant = other.outlineVariant
+    scrim = other.scrim
 }
 
 /**
@@ -553,8 +578,10 @@
         ColorSchemeKeyTokens.OnTertiary -> onTertiary
         ColorSchemeKeyTokens.OnTertiaryContainer -> onTertiaryContainer
         ColorSchemeKeyTokens.Outline -> outline
+        ColorSchemeKeyTokens.OutlineVariant -> outlineVariant
         ColorSchemeKeyTokens.Primary -> primary
         ColorSchemeKeyTokens.PrimaryContainer -> primaryContainer
+        ColorSchemeKeyTokens.Scrim -> scrim
         ColorSchemeKeyTokens.Secondary -> secondary
         ColorSchemeKeyTokens.SecondaryContainer -> secondaryContainer
         ColorSchemeKeyTokens.Surface -> surface
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
index a0c39cc..5dd5a33 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
@@ -46,7 +46,7 @@
 @Composable
 fun Divider(
     modifier: Modifier = Modifier,
-    color: Color = DividerDefaults.Color,
+    color: Color = DividerDefaults.color,
     thickness: Dp = DividerDefaults.Thickness,
     startIndent: Dp = 0.dp
 ) {
@@ -70,9 +70,9 @@
 
 /** Default values for [Divider] */
 object DividerDefaults {
-    /** Default color of a divider. */
-    val Color: Color @Composable get() = DividerTokens.Color.toColor()
-
     /** Default thickness of a divider. */
     val Thickness: Dp = DividerTokens.Thickness
+
+    /** Default color of a divider. */
+    val color: Color @Composable get() = DividerTokens.Color.toColor()
 }
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index af1cfa4..9ea80f4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -94,8 +94,8 @@
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FloatingActionButtonDefaults.Shape,
-    containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+    shape: Shape = FloatingActionButtonDefaults.shape,
+    containerColor: Color = FloatingActionButtonDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
     content: @Composable () -> Unit,
@@ -162,8 +162,8 @@
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FloatingActionButtonDefaults.SmallShape,
-    containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+    shape: Shape = FloatingActionButtonDefaults.smallShape,
+    containerColor: Color = FloatingActionButtonDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
     content: @Composable () -> Unit,
@@ -214,8 +214,8 @@
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FloatingActionButtonDefaults.LargeShape,
-    containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+    shape: Shape = FloatingActionButtonDefaults.largeShape,
+    containerColor: Color = FloatingActionButtonDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
     content: @Composable () -> Unit,
@@ -269,8 +269,8 @@
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FloatingActionButtonDefaults.ExtendedFabShape,
-    containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+    shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
+    containerColor: Color = FloatingActionButtonDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
     content: @Composable RowScope.() -> Unit,
@@ -336,8 +336,8 @@
     modifier: Modifier = Modifier,
     expanded: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FloatingActionButtonDefaults.ExtendedFabShape,
-    containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+    shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
+    containerColor: Color = FloatingActionButtonDefaults.containerColor,
     contentColor: Color = contentColorFor(containerColor),
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
 ) {
@@ -419,27 +419,27 @@
  * Contains the default values used by [FloatingActionButton]
  */
 object FloatingActionButtonDefaults {
-    /** Default shape for a floating action button. */
-    val Shape: Shape @Composable get() = FabPrimaryTokens.ContainerShape.toShape()
-
-    /** Default shape for a small floating action button. */
-    val SmallShape: Shape @Composable get() = FabPrimarySmallTokens.ContainerShape.toShape()
-
-    /** Default shape for a large floating action button. */
-    val LargeShape: Shape @Composable get() = FabPrimaryLargeTokens.ContainerShape.toShape()
-
-    /** Default shape for an extended floating action button. */
-    val ExtendedFabShape: Shape @Composable get() =
-        ExtendedFabPrimaryTokens.ContainerShape.toShape()
-
-    /** Default container color for a floating action button. */
-    val ContainerColor: Color @Composable get() = FabPrimaryTokens.ContainerColor.toColor()
-
     /**
      * The recommended size of the icon inside a [LargeFloatingActionButton].
      */
     val LargeIconSize = FabPrimaryLargeTokens.IconSize
 
+    /** Default shape for a floating action button. */
+    val shape: Shape @Composable get() = FabPrimaryTokens.ContainerShape.toShape()
+
+    /** Default shape for a small floating action button. */
+    val smallShape: Shape @Composable get() = FabPrimarySmallTokens.ContainerShape.toShape()
+
+    /** Default shape for a large floating action button. */
+    val largeShape: Shape @Composable get() = FabPrimaryLargeTokens.ContainerShape.toShape()
+
+    /** Default shape for an extended floating action button. */
+    val extendedFabShape: Shape @Composable get() =
+        ExtendedFabPrimaryTokens.ContainerShape.toShape()
+
+    /** Default container color for a floating action button. */
+    val containerColor: Color @Composable get() = FabPrimaryTokens.ContainerColor.toColor()
+
     /**
      * Creates a [FloatingActionButtonElevation] that represents the elevation of a
      * [FloatingActionButton] in different states. For use cases in which a less prominent
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index fecaaaa..9f39073 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -200,7 +200,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.FilledShape,
+    shape: Shape = IconButtonDefaults.filledShape,
     colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -261,7 +261,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.FilledShape,
+    shape: Shape = IconButtonDefaults.filledShape,
     colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -319,7 +319,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.FilledShape,
+    shape: Shape = IconButtonDefaults.filledShape,
     colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -383,7 +383,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.FilledShape,
+    shape: Shape = IconButtonDefaults.filledShape,
     colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -448,7 +448,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.OutlinedShape,
+    shape: Shape = IconButtonDefaults.outlinedShape,
     border: BorderStroke? = IconButtonDefaults.outlinedIconButtonBorder(enabled),
     colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonColors(),
     content: @Composable () -> Unit
@@ -510,7 +510,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = IconButtonDefaults.OutlinedShape,
+    shape: Shape = IconButtonDefaults.outlinedShape,
     border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
     colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
     content: @Composable () -> Unit
@@ -597,10 +597,10 @@
  */
 object IconButtonDefaults {
     /** Default shape for a filled icon button. */
-    val FilledShape: Shape @Composable get() = FilledIconButtonTokens.ContainerShape.toShape()
+    val filledShape: Shape @Composable get() = FilledIconButtonTokens.ContainerShape.toShape()
 
     /** Default shape for an outlined icon button. */
-    val OutlinedShape: Shape @Composable get() =
+    val outlinedShape: Shape @Composable get() =
         OutlinedIconButtonTokens.ContainerShape.toShape()
 
     /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
index 071b52b..d07e4eb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
@@ -279,9 +279,9 @@
 @ExperimentalMaterial3Api
 private fun ListItem(
     modifier: Modifier = Modifier,
-    shape: Shape = ListItemDefaults.Shape,
-    containerColor: Color = ListItemDefaults.ContainerColor,
-    contentColor: Color = ListItemDefaults.ContentColor,
+    shape: Shape = ListItemDefaults.shape,
+    containerColor: Color = ListItemDefaults.containerColor,
+    contentColor: Color = ListItemDefaults.contentColor,
     tonalElevation: Dp = ListItemDefaults.Elevation,
     shadowElevation: Dp = ListItemDefaults.Elevation,
     content: @Composable RowScope.() -> Unit,
@@ -360,17 +360,17 @@
  */
 @ExperimentalMaterial3Api
 object ListItemDefaults {
-    /** The default shape of a list item */
-    val Shape: Shape @Composable get() = ListTokens.ListItemContainerShape.toShape()
-
     /** The default elevation of a list item */
     val Elevation: Dp = ListTokens.ListItemContainerElevation
 
+    /** The default shape of a list item */
+    val shape: Shape @Composable get() = ListTokens.ListItemContainerShape.toShape()
+
     /** The container color of a list item */
-    val ContainerColor: Color @Composable get() = ListTokens.ListItemContainerColor.toColor()
+    val containerColor: Color @Composable get() = ListTokens.ListItemContainerColor.toColor()
 
     /** The content color of a list item */
-    val ContentColor: Color @Composable get() = ListTokens.ListItemLabelTextColor.toColor()
+    val contentColor: Color @Composable get() = ListTokens.ListItemLabelTextColor.toColor()
 
     /**
      * Creates a [ListItemColors] that represents the default container and content colors used in a
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 42541a1..72f36d4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -92,7 +92,7 @@
 @Composable
 fun NavigationBar(
     modifier: Modifier = Modifier,
-    containerColor: Color = NavigationBarDefaults.ContainerColor,
+    containerColor: Color = NavigationBarDefaults.containerColor,
     contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
     tonalElevation: Dp = NavigationBarDefaults.Elevation,
     content: @Composable RowScope.() -> Unit
@@ -242,11 +242,11 @@
 
 /** Defaults used in [NavigationBar]. */
 object NavigationBarDefaults {
-    /** Default color for a navigation bar. */
-    val ContainerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
-
     /** Default elevation for a navigation bar. */
     val Elevation: Dp = NavigationBarTokens.ContainerElevation
+
+    /** Default color for a navigation bar. */
+    val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
 }
 
 /** Defaults used in [NavigationBarItem]. */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 67fd8a8..a4ff31d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -37,7 +37,6 @@
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.tokens.NavigationDrawerTokens
-import androidx.compose.material3.tokens.PaletteTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Stable
@@ -262,11 +261,11 @@
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     gesturesEnabled: Boolean = true,
-    drawerShape: Shape = DrawerDefaults.Shape,
+    drawerShape: Shape = DrawerDefaults.shape,
     drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
-    drawerContainerColor: Color = DrawerDefaults.ContainerColor,
+    drawerContainerColor: Color = DrawerDefaults.containerColor,
     drawerContentColor: Color = contentColorFor(drawerContainerColor),
-    scrimColor: Color = DrawerDefaults.ScrimColor,
+    scrimColor: Color = DrawerDefaults.scrimColor,
     content: @Composable () -> Unit
 ) {
     val scope = rememberCoroutineScope()
@@ -355,11 +354,11 @@
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     gesturesEnabled: Boolean = true,
-    drawerShape: Shape = DrawerDefaults.Shape,
+    drawerShape: Shape = DrawerDefaults.shape,
     drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
-    drawerContainerColor: Color = DrawerDefaults.ContainerColor,
+    drawerContainerColor: Color = DrawerDefaults.containerColor,
     drawerContentColor: Color = contentColorFor(drawerContainerColor),
-    scrimColor: Color = DrawerDefaults.ScrimColor,
+    scrimColor: Color = DrawerDefaults.scrimColor,
     content: @Composable () -> Unit
 ) {
     ModalNavigationDrawer(
@@ -544,9 +543,6 @@
  */
 @ExperimentalMaterial3Api
 object DrawerDefaults {
-    /** Default shape for a navigation drawer. */
-    val Shape: Shape @Composable get() = NavigationDrawerTokens.ContainerShape.toShape()
-
     /**
      * Default Elevation for drawer container in the [ModalNavigationDrawer] as specified in the
      * Material specification.
@@ -565,13 +561,15 @@
      */
     val DismissibleDrawerElevation = NavigationDrawerTokens.StandardContainerElevation
 
+    /** Default shape for a navigation drawer. */
+    val shape: Shape @Composable get() = NavigationDrawerTokens.ContainerShape.toShape()
+
     /** Default color of the scrim that obscures content when the drawer is open */
-    val ScrimColor: Color
-        @Composable
-        get() = PaletteTokens.NeutralVariant0.copy(alpha = NavigationDrawerTokens.ScrimOpacity)
+    val scrimColor: Color
+        @Composable get() = MaterialTheme.colorScheme.scrim.copy(.32f)
 
     /** Default container color for a navigation drawer */
-    val ContainerColor: Color @Composable get() = NavigationDrawerTokens.ContainerColor.toColor()
+    val containerColor: Color @Composable get() = NavigationDrawerTokens.ContainerColor.toColor()
 }
 
 /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
index 586d240..74cb1fc 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
@@ -31,8 +31,7 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.material.ripple.rememberRipple
@@ -58,6 +57,7 @@
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
@@ -105,8 +105,9 @@
         modifier = modifier,
     ) {
         Column(
-            Modifier.fillMaxHeight()
-                .width(NavigationRailTokens.ContainerWidth)
+            Modifier
+                .fillMaxHeight()
+                .widthIn(min = NavigationRailTokens.ContainerWidth)
                 .padding(vertical = NavigationRailVerticalPadding)
                 .selectableGroup(),
             horizontalAlignment = Alignment.CenterHorizontally,
@@ -185,7 +186,8 @@
                 interactionSource = interactionSource,
                 indication = null,
             )
-            .size(width = NavigationRailItemWidth, height = NavigationRailItemHeight),
+            .height(height = NavigationRailItemHeight)
+            .widthIn(min = NavigationRailItemWidth),
         contentAlignment = Alignment.Center
     ) {
         val animationProgress: Float by animateFloatAsState(
@@ -216,14 +218,16 @@
         // ripple, which is why they are separate composables
         val indicatorRipple = @Composable {
             Box(
-                Modifier.layoutId(IndicatorRippleLayoutIdTag)
+                Modifier
+                    .layoutId(IndicatorRippleLayoutIdTag)
                     .clip(indicatorShape)
                     .indication(offsetInteractionSource, rememberRipple())
             )
         }
         val indicator = @Composable {
             Box(
-                Modifier.layoutId(IndicatorLayoutIdTag)
+                Modifier
+                    .layoutId(IndicatorLayoutIdTag)
                     .background(
                         color = colors.indicatorColor.copy(alpha = animationProgress),
                         shape = indicatorShape
@@ -370,7 +374,8 @@
 
         if (label != null) {
             Box(
-                Modifier.layoutId(LabelLayoutIdTag)
+                Modifier
+                    .layoutId(LabelLayoutIdTag)
                     .alpha(if (alwaysShowLabel) 1f else animationProgress)
             ) { label() }
         }
@@ -442,7 +447,13 @@
     indicatorPlaceable: Placeable?,
     constraints: Constraints,
 ): MeasureResult {
-    val width = constraints.maxWidth
+    val width = constraints.constrainWidth(
+        maxOf(
+            iconPlaceable.width,
+            indicatorRipplePlaceable.width,
+            indicatorPlaceable?.width ?: 0
+        )
+    )
     val height = constraints.maxHeight
 
     val iconX = (width - iconPlaceable.width) / 2
@@ -519,8 +530,13 @@
     // The interpolated fraction of iconDistance that all placeables need to move based on
     // animationProgress, since the icon is higher in the selected state.
     val offset = (iconDistance * (1 - animationProgress)).roundToInt()
-
-    val width = constraints.maxWidth
+    val width = constraints.constrainWidth(
+        maxOf(
+            iconPlaceable.width,
+            labelPlaceable.width,
+            indicatorPlaceable?.width ?: 0
+        )
+    )
     val labelX = (width - labelPlaceable.width) / 2
     val iconX = (width - iconPlaceable.width) / 2
     val rippleX = (width - indicatorRipplePlaceable.width) / 2
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 5f4f735..3e59495 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -147,7 +147,7 @@
     singleLine: Boolean = false,
     maxLines: Int = Int.MAX_VALUE,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.OutlinedShape,
+    shape: Shape = TextFieldDefaults.outlinedShape,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
 ) {
     // If color is not provided via the text style, use content color as a default
@@ -292,7 +292,7 @@
     singleLine: Boolean = false,
     maxLines: Int = Int.MAX_VALUE,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.OutlinedShape,
+    shape: Shape = TextFieldDefaults.outlinedShape,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
 ) {
     // If color is not provided via the text style, use content color as a default
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index 6cc6944..945b55c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -73,8 +73,8 @@
 fun LinearProgressIndicator(
     progress: Float,
     modifier: Modifier = Modifier,
-    color: Color = ProgressIndicatorDefaults.LinearColor,
-    trackColor: Color = ProgressIndicatorDefaults.LinearTrackColor,
+    color: Color = ProgressIndicatorDefaults.linearColor,
+    trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
 ) {
     Canvas(
         modifier
@@ -105,8 +105,8 @@
 @Composable
 fun LinearProgressIndicator(
     modifier: Modifier = Modifier,
-    color: Color = ProgressIndicatorDefaults.LinearColor,
-    trackColor: Color = ProgressIndicatorDefaults.LinearTrackColor,
+    color: Color = ProgressIndicatorDefaults.linearColor,
+    trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
 ) {
     val infiniteTransition = rememberInfiniteTransition()
     // Fractional position of the 'head' and 'tail' of the two lines drawn, i.e. if the head is 0.8
@@ -230,7 +230,7 @@
 fun CircularProgressIndicator(
     progress: Float,
     modifier: Modifier = Modifier,
-    color: Color = ProgressIndicatorDefaults.CircularColor,
+    color: Color = ProgressIndicatorDefaults.circularColor,
     strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth
 ) {
     val stroke = with(LocalDensity.current) {
@@ -265,7 +265,7 @@
 @Composable
 fun CircularProgressIndicator(
     modifier: Modifier = Modifier,
-    color: Color = ProgressIndicatorDefaults.CircularColor,
+    color: Color = ProgressIndicatorDefaults.circularColor,
     strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth
 ) {
     val stroke = with(LocalDensity.current) {
@@ -398,15 +398,15 @@
  */
 object ProgressIndicatorDefaults {
     /** Default color for a linear progress indicator. */
-    val LinearColor: Color @Composable get() =
+    val linearColor: Color @Composable get() =
         LinearProgressIndicatorTokens.ActiveIndicatorColor.toColor()
 
     /** Default color for a circular progress indicator. */
-    val CircularColor: Color @Composable get() =
+    val circularColor: Color @Composable get() =
         CircularProgressIndicatorTokens.ActiveIndicatorColor.toColor()
 
     /** Default track color for a linear progress indicator. */
-    val LinearTrackColor: Color @Composable get() =
+    val linearTrackColor: Color @Composable get() =
         LinearProgressIndicatorTokens.TrackColor.toColor()
 
     /** Default stroke width for a circular progress indicator. */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
index 6e6ef5e..86cc3b3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -94,11 +94,11 @@
     action: @Composable (() -> Unit)? = null,
     dismissAction: @Composable (() -> Unit)? = null,
     actionOnNewLine: Boolean = false,
-    shape: Shape = SnackbarDefaults.Shape,
-    containerColor: Color = SnackbarDefaults.Color,
-    contentColor: Color = SnackbarDefaults.ContentColor,
-    actionContentColor: Color = SnackbarDefaults.ActionContentColor,
-    dismissActionContentColor: Color = SnackbarDefaults.DismissActionContentColor,
+    shape: Shape = SnackbarDefaults.shape,
+    containerColor: Color = SnackbarDefaults.color,
+    contentColor: Color = SnackbarDefaults.contentColor,
+    actionContentColor: Color = SnackbarDefaults.actionContentColor,
+    dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
     content: @Composable () -> Unit
 ) {
     Surface(
@@ -196,12 +196,12 @@
     snackbarData: SnackbarData,
     modifier: Modifier = Modifier,
     actionOnNewLine: Boolean = false,
-    shape: Shape = SnackbarDefaults.Shape,
-    containerColor: Color = SnackbarDefaults.Color,
-    contentColor: Color = SnackbarDefaults.ContentColor,
-    actionColor: Color = SnackbarDefaults.ActionColor,
-    actionContentColor: Color = SnackbarDefaults.ActionContentColor,
-    dismissActionContentColor: Color = SnackbarDefaults.DismissActionContentColor,
+    shape: Shape = SnackbarDefaults.shape,
+    containerColor: Color = SnackbarDefaults.color,
+    contentColor: Color = SnackbarDefaults.contentColor,
+    actionColor: Color = SnackbarDefaults.actionColor,
+    actionContentColor: Color = SnackbarDefaults.actionContentColor,
+    dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
 ) {
     val actionLabel = snackbarData.visuals.actionLabel
     val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
@@ -403,22 +403,22 @@
  */
 object SnackbarDefaults {
     /** Default shape of a snackbar. */
-    val Shape: Shape @Composable get() = SnackbarTokens.ContainerShape.toShape()
+    val shape: Shape @Composable get() = SnackbarTokens.ContainerShape.toShape()
 
     /** Default color of a snackbar. */
-    val Color: Color @Composable get() = SnackbarTokens.ContainerColor.toColor()
+    val color: Color @Composable get() = SnackbarTokens.ContainerColor.toColor()
 
     /** Default content color of a snackbar. */
-    val ContentColor: Color @Composable get() = SnackbarTokens.SupportingTextColor.toColor()
+    val contentColor: Color @Composable get() = SnackbarTokens.SupportingTextColor.toColor()
 
     /** Default action color of a snackbar. */
-    val ActionColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
+    val actionColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
 
     /** Default action content color of a snackbar. */
-    val ActionContentColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
+    val actionContentColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
 
     /** Default dismiss action content color of a snackbar. */
-    val DismissActionContentColor: Color @Composable get() = SnackbarTokens.IconColor.toColor()
+    val dismissActionContentColor: Color @Composable get() = SnackbarTokens.IconColor.toColor()
 }
 
 private val ContainerMaxWidth = 600.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
index 6db393d..306b2f2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
@@ -123,7 +123,7 @@
     DisposableEffect(checked) {
         if (offset.targetValue != targetValue) {
             scope.launch {
-                offset.animateTo(targetValue)
+                offset.animateTo(targetValue, AnimationSpec)
             }
         }
         onDispose { }
@@ -134,12 +134,7 @@
         if (onCheckedChange != null) {
             Modifier.toggleable(
                 value = checked,
-                 value: Boolean ->
-                    onCheckedChange(value)
-                    scope.launch {
-                        offset.animateTo(valueToOffset(value), AnimationSpec)
-                    }
-                },
+                >
                 enabled = enabled,
                 role = Role.Switch,
                 interactionSource = interactionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index ddc2e93..66afa3a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -126,8 +126,8 @@
 fun TabRow(
     selectedTabIndex: Int,
     modifier: Modifier = Modifier,
-    containerColor: Color = TabRowDefaults.Color,
-    contentColor: Color = TabRowDefaults.ContentColor,
+    containerColor: Color = TabRowDefaults.containerColor,
+    contentColor: Color = TabRowDefaults.contentColor,
     indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
         TabRowDefaults.Indicator(
             Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
@@ -214,8 +214,8 @@
 fun ScrollableTabRow(
     selectedTabIndex: Int,
     modifier: Modifier = Modifier,
-    containerColor: Color = TabRowDefaults.Color,
-    contentColor: Color = TabRowDefaults.ContentColor,
+    containerColor: Color = TabRowDefaults.containerColor,
+    contentColor: Color = TabRowDefaults.contentColor,
     edgePadding: Dp = ScrollableTabRowPadding,
     indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
         TabRowDefaults.Indicator(
@@ -343,10 +343,11 @@
  */
 object TabRowDefaults {
     /** Default color of a tab row. */
-    val Color: Color @Composable get() = PrimaryNavigationTabTokens.ContainerColor.toColor()
+    val containerColor: Color @Composable get() =
+        PrimaryNavigationTabTokens.ContainerColor.toColor()
 
     /** Default content color of a tab row. */
-    val ContentColor: Color @Composable get() =
+    val contentColor: Color @Composable get() =
         PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
 
     /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index a023710..63fb471 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -173,7 +173,7 @@
     singleLine: Boolean = false,
     maxLines: Int = Int.MAX_VALUE,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.FilledShape,
+    shape: Shape = TextFieldDefaults.filledShape,
     colors: TextFieldColors = TextFieldDefaults.textFieldColors()
 ) {
     // If color is not provided via the text style, use content color as a default
@@ -308,7 +308,7 @@
     singleLine: Boolean = false,
     maxLines: Int = Int.MAX_VALUE,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = TextFieldDefaults.FilledShape,
+    shape: Shape = TextFieldDefaults.filledShape,
     colors: TextFieldColors = TextFieldDefaults.textFieldColors()
 ) {
     // If color is not provided via the text style, use content color as a default
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 4a21219..4461392 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -167,10 +167,10 @@
 @Immutable
 object TextFieldDefaults {
     /** Default shape for an outlined text field. */
-    val OutlinedShape: Shape @Composable get() = OutlinedTextFieldTokens.ContainerShape.toShape()
+    val outlinedShape: Shape @Composable get() = OutlinedTextFieldTokens.ContainerShape.toShape()
 
     /** Default shape for a filled text field. */
-    val FilledShape: Shape @Composable get() = FilledTextFieldTokens.ContainerShape.toShape()
+    val filledShape: Shape @Composable get() = FilledTextFieldTokens.ContainerShape.toShape()
 
     /**
      * The default min width applied for a [TextField] and [OutlinedTextField].
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
index c7f7094..82a7273 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_103
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
     val >
     val >
     val Outline = PaletteTokens.NeutralVariant60
+    val OutlineVariant = PaletteTokens.NeutralVariant30
     val Primary = PaletteTokens.Primary80
     val PrimaryContainer = PaletteTokens.Primary30
+    val Scrim = PaletteTokens.Neutral0
     val Secondary = PaletteTokens.Secondary80
     val SecondaryContainer = PaletteTokens.Secondary30
     val Surface = PaletteTokens.Neutral10
@@ -46,4 +48,4 @@
     val SurfaceVariant = PaletteTokens.NeutralVariant30
     val Tertiary = PaletteTokens.Tertiary80
     val TertiaryContainer = PaletteTokens.Tertiary30
-}
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
index 54a28b6..765b65d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_103
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
     val >
     val >
     val Outline = PaletteTokens.NeutralVariant50
+    val OutlineVariant = PaletteTokens.NeutralVariant80
     val Primary = PaletteTokens.Primary40
     val PrimaryContainer = PaletteTokens.Primary90
+    val Scrim = PaletteTokens.Neutral0
     val Secondary = PaletteTokens.Secondary40
     val SecondaryContainer = PaletteTokens.Secondary90
     val Surface = PaletteTokens.Neutral99
@@ -46,4 +48,4 @@
     val SurfaceVariant = PaletteTokens.NeutralVariant90
     val Tertiary = PaletteTokens.Tertiary40
     val TertiaryContainer = PaletteTokens.Tertiary90
-}
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
index ee5bf8f..c1c1104 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_103
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
     OnTertiary,
     OnTertiaryContainer,
     Outline,
+    OutlineVariant,
     Primary,
     PrimaryContainer,
+    Scrim,
     Secondary,
     SecondaryContainer,
     Surface,
@@ -46,4 +48,4 @@
     SurfaceVariant,
     Tertiary,
     TertiaryContainer,
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 241e5cd..86a6f47 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -406,6 +406,33 @@
                 shapeColor = Color.Red
             )
     }
+
+    @Test
+    fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+        var childModifier by mutableStateOf(Modifier as Modifier)
+
+        rule.setContent {
+            LayoutUsingIntrinsics() {
+                Box(
+                    Modifier
+                        .testTag("child")
+                        .then(childModifier)
+                )
+            }
+        }
+
+        rule.onNodeWithTag("child")
+            .assertWidthIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(0.dp)
+
+        rule.runOnIdle {
+            childModifier = Modifier.withIntrinsics(30.dp, 20.dp)
+        }
+
+        rule.onNodeWithTag("child")
+            .assertWidthIsEqualTo(30.dp)
+            .assertHeightIsEqualTo(20.dp)
+    }
 }
 
 @Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9344d54..0885058 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.viewinterop
 
 import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
 import android.os.Build
 import android.os.Bundle
 import android.os.Parcelable
@@ -33,6 +35,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
@@ -45,8 +48,10 @@
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
@@ -272,7 +277,10 @@
             }
         }
         rule.setContent {
-            AndroidView({ frameLayout }, Modifier.testTag("view").background(color = Color.Blue))
+            AndroidView({ frameLayout },
+                Modifier
+                    .testTag("view")
+                    .background(color = Color.Blue))
         }
 
         rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
@@ -356,9 +364,11 @@
             CompositionLocalProvider(LocalDensity provides density) {
                 AndroidView(
                     { FrameLayout(it) },
-                    Modifier.requiredSize(size).onGloballyPositioned {
-                        assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
-                    }
+                    Modifier
+                        .requiredSize(size)
+                        .onGloballyPositioned {
+                            assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+                        }
                 )
             }
         }
@@ -566,7 +576,11 @@
         val sizeDp = with(rule.density) { size.toDp() }
         rule.setContent {
             Column {
-                Box(Modifier.size(sizeDp).background(Color.Blue).testTag("box"))
+                Box(
+                    Modifier
+                        .size(sizeDp)
+                        .background(Color.Blue)
+                        .testTag("box"))
                 AndroidView(factory = { SurfaceView(it) })
             }
         }
@@ -619,6 +633,40 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun androidView_noClip() {
+        rule.setContent {
+            Box(Modifier.fillMaxSize().background(Color.White)) {
+                with(LocalDensity.current) {
+                    Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+                        Box(
+                            Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+                        ) {
+                            AndroidView(factory = { context ->
+                                object : View(context) {
+                                    init {
+                                        clipToOutline = false
+                                    }
+
+                                    override fun onDraw(canvas: Canvas) {
+                                        val paint = Paint()
+                                        paint.color = Color.Blue.toArgb()
+                                        paint.style = Paint.Style.FILL
+                                        canvas.drawRect(0f, 0f, 150f, 150f, paint)
+                                    }
+                                }
+                            })
+                        }
+                    }
+                }
+            }
+        }
+        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
+            Color.Blue
+        }
+    }
+
     private class StateSavingView(
         private val key: String,
         private val value: String,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
index 2d81082..36a4079 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
@@ -40,4 +40,11 @@
      * Called when IME triggered a KeyEvent
      */
     fun onKeyEvent(event: KeyEvent)
+
+    /**
+     * Called when IME closed the input connection.
+     *
+     * @param ic a closed input connection
+     */
+    fun onConnectionClosed(ic: RecordingInputConnection)
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
index 66a63a2..97ee595 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
@@ -168,6 +168,7 @@
         editCommands.clear()
         batchDepth = 0
         isActive = false
+        eventCallback.onConnectionClosed(this)
     }
 
     // /////////////////////////////////////////////////////////////////////////////////////////////
@@ -276,7 +277,8 @@
         if (DEBUG) {
             with(extractedText) {
                 logDebug(
-                    "getExtractedText() return: text: $text" +
+
+                    "getExtractedText() return: text: \"$text\"" +
                         ",partialStartOffset $partialStartOffset" +
                         ",partialEndOffset $partialEndOffset" +
                         ",selectionStart $selectionStart" +
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 83b2f84..1dbb467 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
 import androidx.core.view.inputmethod.EditorInfoCompat
+import java.lang.ref.WeakReference
 import kotlin.math.roundToInt
 import kotlinx.coroutines.channels.Channel
 
@@ -72,7 +73,12 @@
     internal var state = TextFieldValue(text = "", selection = TextRange.Zero)
         private set
     private var imeOptions = ImeOptions.Default
-    private var ic: RecordingInputConnection? = null
+
+    // RecordingInputConnection has strong reference to the View through TextInputServiceAndroid and
+    // event callback. The connection should be closed when IME has changed and removed from this
+    // list in onConnectionClosed callback, but not clear it is guaranteed the close connection is
+    // called any time. So, keep it in WeakReference just in case.
+    private var ics = mutableListOf<WeakReference<RecordingInputConnection>>()
 
     // used for sendKeyEvent delegation
     private val baseInputConnection by lazy(LazyThreadSafetyMode.NONE) {
@@ -120,10 +126,19 @@
                 override fun onKeyEvent(event: KeyEvent) {
                     baseInputConnection.sendKeyEvent(event)
                 }
+
+                override fun onConnectionClosed(ic: RecordingInputConnection) {
+                    for (i in 0 until ics.size) {
+                        if (ics[i].get() == ic) {
+                            ics.removeAt(i)
+                            return // No duplicated instances should be in the list.
+                        }
+                    }
+                }
             }
         ).also {
-            ic = it
-            if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ic") }
+            ics.add(WeakReference(it))
+            if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics") }
         }
     }
 
@@ -298,7 +313,9 @@
             this.state.composition != newValue.composition
         this.state = newValue
         // update the latest TextFieldValue in InputConnection
-        ic?.mTextFieldValue = newValue
+        for (i in 0 until ics.size) {
+            ics[i].get()?.mTextFieldValue = newValue
+        }
 
         if (oldValue == newValue) {
             if (DEBUG) {
@@ -330,7 +347,9 @@
         if (restartInput) {
             restartInputImmediately()
         } else {
-            ic?.updateInputState(this.state, inputMethodManager, view)
+            for (i in 0 until ics.size) {
+                ics[i].get()?.updateInputState(this.state, inputMethodManager, view)
+            }
         }
     }
 
@@ -349,7 +368,7 @@
         // use, i.e. InputConnection has created.
         // Even if we miss all the timing of requesting rectangle during initial text field focus,
         // focused rectangle will be requested when software keyboard has shown.
-        if (ic == null) {
+        if (ics.isEmpty()) {
             focusedRect?.let {
                 // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
                 // create another Rect and then pass it.
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index fe922bc..9b725ee 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -167,6 +167,10 @@
 
     override val viewRoot: View get() = this
 
+    init {
+        clipChildren = false
+    }
+
     var factory: ((Context) -> T)? = null
         set(value) {
             field = value
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 36e5380..95b9e85 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -299,6 +299,7 @@
 # > Task :glance:glance:reportLibraryMetrics
 Info: Stripped invalid locals information from [0-9]+ methods?\.
 Info: Methods with invalid locals information:
+void androidx\.tv\.foundation\.lazy\.list\.LazyListKt\.LazyList\(androidx\.compose\.ui\.Modifier, androidx\.tv\.foundation\.lazy\.list\.TvLazyListState, androidx\.compose\.foundation\.layout\.PaddingValues, boolean, boolean, boolean, androidx\.tv\.foundation\.PivotOffsets, androidx\.compose\.ui\.Alignment\$Horizontal, androidx\.compose\.foundation\.layout\.Arrangement\$Vertical, androidx\.compose\.ui\.Alignment\$Vertical, androidx\.compose\.foundation\.layout\.Arrangement\$Horizontal, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.runtime\.Composer, int, int, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.animation\.AnimationModifierKt\$animateContentSize\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.material[0-9]+\.SliderKt\$sliderTapModifier\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.animation\.demos\.layoutanimation\.AnimatedPlacementDemoKt\$animatePlacement\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
@@ -321,6 +322,7 @@
 void androidx\.compose\.foundation\.demos\.relocation\.BringIntoViewAndroidInteropDemoKt\.BringIntoViewAndroidInteropDemo\(androidx\.compose\.runtime\.Composer, int\)
 void androidx\.compose\.ui\.demos\.keyinput\.InterceptEnterToSendMessageDemoKt\.InterceptEnterToSendMessageDemo\(androidx\.compose\.runtime\.Composer, int\)
 Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint [\-A-Z]*\.
+void androidx\.tv\.foundation\.lazy\.grid\.LazyGridKt\.LazyGrid\(androidx\.compose\.ui\.Modifier, androidx\.tv\.foundation\.lazy\.grid\.TvLazyGridState, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.foundation\.layout\.PaddingValues, boolean, boolean, boolean, androidx\.compose\.foundation\.layout\.Arrangement\$Vertical, androidx\.compose\.foundation\.layout\.Arrangement\$Horizontal, androidx\.tv\.foundation\.PivotOffsets, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.runtime\.Composer, int, int, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.material\.SliderKt\$sliderTapModifier\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.foundation\.FocusableKt\$focusable\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
 androidx\.compose\.ui\.Modifier androidx\.compose\.foundation\.ScrollKt\$scroll\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
@@ -439,6 +441,8 @@
 # > Task :tv:tv-material:processReleaseManifest
 # > Task :tv:tv-foundation:processReleaseManifest
 package="androidx\.tv\..*" found in source AndroidManifest\.xml: \$OUT_DIR/androidx/tv/tv\-[a-z]+/build/intermediates/tmp/ProcessLibraryManifest/[a-z]+/tempAndroidManifest[0-9]+\.xml\.
+void androidx.tv.foundation.lazy.list.LazyListKt.LazyList(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.list.TvLazyListState, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.tv.foundation.PivotOffsets, androidx.compose.ui.Alignment$Horizontal, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.ui.Alignment$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
+void androidx.tv.foundation.lazy.grid.LazyGridKt.LazyGrid(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.grid.TvLazyGridState, kotlin.jvm.functions.Function2, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, androidx.tv.foundation.PivotOffsets, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
 # > Task :room:integration-tests:room-testapp:mergeDexWithExpandProjectionDebugAndroidTest
 WARNING:D[0-9]+: Application does not contain `androidx\.tracing\.Trace` as referenced in main\-dex\-list\.
 # > Task :hilt:hilt-compiler:kaptTestKotlin
@@ -468,3 +472,4 @@
 # > Task :buildSrc-tests:test
 WARNING: Illegal reflective access using Lookup on org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper .* to class java\.lang\.ClassLoader
 WARNING: Please consider reporting this to the maintainers of org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper
+
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 91ba9a3..f12d43e 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -48,7 +48,7 @@
 #
 # Each directory's spelling must match the library's directory in
 # frameworks/support.
-readonly javaLibraryDirs=(
+readonly javaLibraryDirsThatDontUseDackka=(
   "android/support/v4"
   "androidx/ads"
   "androidx/appcompat"
@@ -71,7 +71,6 @@
   "androidx/heifwriter"
   "androidx/hilt"
   "androidx/leanback"
-  "androidx/loader"
   "androidx/media"
   "androidx/media2"
   "androidx/mediarouter"
@@ -98,7 +97,7 @@
   "androidx/webkit"
   "androidx/work"
 )
-readonly kotlinLibraryDirs=(
+readonly kotlinLibraryDirsThatDontUseDackka=(
   "android/support/v4"
   "androidx/ads"
   "androidx/appcompat"
@@ -123,7 +122,6 @@
   "androidx/heifwriter"
   "androidx/hilt"
   "androidx/leanback"
-  "androidx/loader"
   "androidx/media"
   "androidx/media2"
   "androidx/mediarouter"
@@ -236,13 +234,13 @@
 # generated by Doclava/Dokka (such as Java refdocs based on Kotlin sources)
 
 cd $outDir
-for dir in "${javaLibraryDirs[@]}"
+for dir in "${javaLibraryDirsThatDontUseDackka[@]}"
 do
   printf "Copying Java refdocs for $dir\n"
   cp -r $doclavaNewDir/reference/$dir/* $newDir/reference/$dir
 done
 
-for dir in "${kotlinLibraryDirs[@]}"
+for dir in "${kotlinLibraryDirsThatDontUseDackka[@]}"
 do
   printf "Copying Kotlin refdocs for $dir\n"
   cp -r $dokkaNewDir/reference/kotlin/$dir/* $newDir/reference/kotlin/$dir
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 5f4aeb0..b6e253c 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -140,9 +140,9 @@
     docs("androidx.enterprise:enterprise-feedback:1.1.0")
     docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
     docs("androidx.exifinterface:exifinterface:1.3.3")
-    docs("androidx.fragment:fragment:1.5.0")
-    docs("androidx.fragment:fragment-ktx:1.5.0")
-    docs("androidx.fragment:fragment-testing:1.5.0")
+    docs("androidx.fragment:fragment:1.5.1")
+    docs("androidx.fragment:fragment-ktx:1.5.1")
+    docs("androidx.fragment:fragment-testing:1.5.1")
     docs("androidx.glance:glance:1.0.0-alpha03")
     docs("androidx.glance:glance-appwidget:1.0.0-alpha03")
     docs("androidx.glance:glance-appwidget-proto:1.0.0-alpha03")
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
index 500c462..b852d05 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
@@ -28,7 +28,9 @@
 import com.intellij.psi.PsiType
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
 import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
 
 /**
  * Lint check for detecting calls to the suspend `repeatOnLifecycle` APIs using `lifecycleOwner`
@@ -58,12 +60,27 @@
 
     override fun getApplicableMethodNames() = listOf("repeatOnLifecycle")
 
+    private val lifecycleMethods = setOf(
+        "onCreateView", "onViewCreated", "onActivityCreated",
+        "onViewStateRestored"
+    )
+
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
         // Check that repeatOnLifecycle is called in a Fragment
-        if (!hasFragmentAsAncestorType(node.getParentOfType<UClass>())) return
+        if (!hasFragmentAsAncestorType(node.getParentOfType())) return
 
-        // Report issue if the receiver is not using viewLifecycleOwner
-        if (node.receiver?.sourcePsi?.text?.contains(SAFE_RECEIVER, ignoreCase = true) != true) {
+        // Check that repeatOnLifecycle is called in the proper Lifecycle function
+        if (!isCalledInViewLifecycleFunction(node.getParentOfType())) return
+
+        // Look at the entire launch scope
+        var launchScope = node.getParentOfType<KotlinUFunctionCallExpression>()?.receiver
+        while (
+            launchScope != null && !containsViewLifecycleOwnerCall(launchScope.sourcePsi?.text)
+        ) {
+            launchScope = launchScope.getParentOfType()
+        }
+        // Report issue if there is no viewLifecycleOwner in the launch scope
+        if (!containsViewLifecycleOwnerCall(launchScope?.sourcePsi?.text)) {
             context.report(
                 ISSUE,
                 context.getLocation(node),
@@ -72,6 +89,17 @@
         }
     }
 
+    private fun containsViewLifecycleOwnerCall(sourceText: String?): Boolean {
+        if (sourceText == null) return false
+        return sourceText.contains(VIEW_LIFECYCLE_KOTLIN_PROP, ignoreCase = true) ||
+            sourceText.contains(VIEW_LIFECYCLE_FUN, ignoreCase = true)
+    }
+
+    private fun isCalledInViewLifecycleFunction(uMethod: UMethod?): Boolean {
+        if (uMethod == null) return false
+        return lifecycleMethods.contains(uMethod.name)
+    }
+
     /**
      * Check if `uClass` has FRAGMENT as a super type but not DIALOG_FRAGMENT
      */
@@ -91,6 +119,7 @@
     }
 }
 
-private const val SAFE_RECEIVER = "viewLifecycleOwner"
+private const val VIEW_LIFECYCLE_KOTLIN_PROP = "viewLifecycleOwner"
+private const val VIEW_LIFECYCLE_FUN = "getViewLifecycleOwner"
 private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
 private const val DIALOG_FRAGMENT_CLASS = "androidx.fragment.app.DialogFragment"
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
index 93b8bfc..2de49ca 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
@@ -202,4 +202,66 @@
             .run()
             .expectClean()
     }
+
+    @Test
+    fun `viewLifecycleOwner in with outside of launch`() {
+        lint().files(
+            *REPEAT_ON_LIFECYCLE_STUBS,
+            kotlin(
+                """
+                    package foo
+                    
+                    import androidx.lifecycle.Lifecycle
+                    import androidx.lifecycle.LifecycleOwner
+                    import androidx.lifecycle.repeatOnLifecycle
+                    import kotlinx.coroutines.CoroutineScope
+                    import kotlinx.coroutines.GlobalScope
+                    import androidx.fragment.app.Fragment
+                    
+                    class MyFragment : Fragment() {
+                        fun onCreateView() {
+                            with(viewLifecycleOwner) {
+                                lifecycleScope.launch {
+                                    repeatOnLifecycle(Lifecycle.State.STARTED) {}
+                                }       
+                            }               
+                        }
+                    }
+                """.trimIndent()
+            )
+        )
+            .allowCompilationErrors(false)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `viewLifecycleOwner scope directly`() {
+        lint().files(
+            *REPEAT_ON_LIFECYCLE_STUBS,
+            kotlin(
+                """
+                    package foo
+                    
+                    import androidx.lifecycle.Lifecycle
+                    import androidx.lifecycle.LifecycleOwner
+                    import androidx.lifecycle.repeatOnLifecycle
+                    import kotlinx.coroutines.CoroutineScope
+                    import kotlinx.coroutines.GlobalScope
+                    import androidx.fragment.app.Fragment
+                    
+                    class MyFragment : Fragment() {
+                        fun onCreateView() {
+                            viewLifecycleOwner.lifecycleScope.launch {
+                                viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {}
+                            }                    
+                        }
+                    }
+                """.trimIndent()
+            )
+        )
+            .allowCompilationErrors(false)
+            .run()
+            .expectClean()
+    }
 }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97b429e..dc99e9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,7 +150,7 @@
 multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
 nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
 okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
-okio = { module = "com.squareup.okio:okio", version = "3.0.0" }
+okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
 playCore = { module = "com.google.android.play:core", version = "1.10.3" }
 playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
 playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c8f18a2..16af0f1 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -461,8 +461,7 @@
       </trusted-keys>
    </configuration>
    <components>
-      <!-- Unsigned -->
-      <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1">
+      <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1" androidx:reason="Unsigned">
          <artifact name="backport-util-concurrent-3.1.jar">
             <sha256 value="f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902" origin="Generated by Gradle"/>
          </artifact>
@@ -470,8 +469,7 @@
             <sha256 value="770471090ca40a17b9e436ee2ec00819be42042da6f4085ece1d37916dc08ff9" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="classworlds" name="classworlds" version="1.1-alpha-2">
+      <component group="classworlds" name="classworlds" version="1.1-alpha-2" androidx:reason="Unsigned">
          <artifact name="classworlds-1.1-alpha-2.jar">
             <sha256 value="2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3" origin="Generated by Gradle"/>
          </artifact>
@@ -479,8 +477,7 @@
             <sha256 value="0cc647963b74ad1d7a37c9868e9e5a8f474e49297e1863582253a08a4c719cb1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gundy/semver4j/issues/6 -->
-      <component group="com.github.gundy" name="semver4j" version="0.16.4">
+      <component group="com.github.gundy" name="semver4j" version="0.16.4" androidx:reason="Unsigned https://github.com/gundy/semver4j/issues/6">
          <artifact name="semver4j-0.16.4-nodeps.jar">
             <sha256 value="3f59eca516374ccd4fd3551625bf50f8a4b191f700508f7ce4866460a6128af0" origin="Generated by Gradle"/>
          </artifact>
@@ -489,8 +486,7 @@
             <sha256 value="32001db2443b339dd21f5b79ff29d1ade722d1ba080c214bde819f0f72d1604d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google" name="google" version="1">
+      <component group="com.google" name="google" version="1" androidx:reason="Unsigned">
          <artifact name="google-1.pom">
             <sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle"/>
          </artifact>
@@ -543,8 +539,7 @@
             <sha256 value="c6898b1f71e69b15bf90c31fc3ef2de1cffbf454a770700f755b5a47ea48b540" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.code.findbugs" name="jsr305" version="1.3.9">
+      <component group="com.google.code.findbugs" name="jsr305" version="1.3.9" androidx:reason="Unsigned">
          <artifact name="jsr305-1.3.9.jar">
             <sha256 value="905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed" origin="Generated by Gradle"/>
          </artifact>
@@ -552,8 +547,7 @@
             <sha256 value="feab9191311c3d7aeef2b66d6064afc80d3d1d52d980fb07ae43c78c987ba93a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.code.findbugs" name="jsr305" version="2.0.1">
+      <component group="com.google.code.findbugs" name="jsr305" version="2.0.1" androidx:reason="Unsigned">
          <artifact name="jsr305-2.0.1.jar">
             <sha256 value="1e7f53fa5b8b5c807e986ba335665da03f18d660802d8bf061823089d1bee468" origin="Generated by Gradle"/>
          </artifact>
@@ -561,8 +555,7 @@
             <sha256 value="02c12c3c2ae12dd475219ff691c82a4d9ea21f44bc594a181295bf6d43dcfbb0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.prefab" name="cli" version="2.0.0">
+      <component group="com.google.prefab" name="cli" version="2.0.0" androidx:reason="Unsigned">
          <artifact name="cli-2.0.0-all.jar">
             <sha256 value="d9bd89f68446b82be038aae774771ad85922d0b375209b17625a2734b5317e29" origin="Generated by Gradle"/>
          </artifact>
@@ -570,8 +563,7 @@
             <sha256 value="4856401a263b39c5394b36a16e0d99628cf05c68008a0cda9691c72bb101e1df" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.googlecode.json-simple" name="json-simple" version="1.1">
+      <component group="com.googlecode.json-simple" name="json-simple" version="1.1" androidx:reason="Unsigned">
          <artifact name="json-simple-1.1.jar">
             <sha256 value="2d9484f4c649f708f47f9a479465fc729770ee65617dca3011836602264f6439" origin="Generated by Gradle"/>
          </artifact>
@@ -579,20 +571,17 @@
             <sha256 value="47a89be0fa0fedd476db5fd2c83487654d2a119c391f83a142be876667cf7dab" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
-      <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2">
+      <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
          <artifact name="common-custom-user-data-gradle-plugin-1.7.2.pom">
             <sha256 value="c70db912c8b127b1b9a6c0cccac1a9353e9fc3b063a3be0114a5208f43c09c31" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
-      <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2">
+      <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
          <artifact name="gradle-enterprise-gradle-plugin-3.10.2.pom">
             <sha256 value="57603c9a75a9ef86ce30b1cb2db728d3cd9caf1be967343f1fc2316c85df5653" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.okio" name="okio" version="2.8.0">
+      <component group="com.squareup.okio" name="okio" version="2.8.0" androidx:reason="Unsigned">
          <artifact name="okio-2.8.0.module">
             <sha256 value="17baab7270389a5fa63ab12811864d0a00f381611bc4eb042fa1bd5918ed0965" origin="Generated by Gradle"/>
          </artifact>
@@ -600,20 +589,17 @@
             <sha256 value="4496b06e73982fcdd8a5393f46e5df2ce2fa4465df5895454cac68a32f09bbc8" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.okio" name="okio" version="2.10.0">
+      <component group="com.squareup.okio" name="okio" version="2.10.0" androidx:reason="Unsigned">
          <artifact name="okio-jvm-2.10.0.jar">
             <sha256 value="a27f091d34aa452e37227e2cfa85809f29012a8ef2501a9b5a125a978e4fcbc1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0">
+      <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0" androidx:reason="Unsigned">
          <artifact name="sqldelight-coroutines-extensions-jvm-1.3.0.jar">
             <sha256 value="47305eab44f8b2aef533d8ce76cec9eb5175715cac26b538b6bff5b106ed0ba1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-grpc-client-3.6.0.module">
             <sha256 value="f4d91b43e5ce4603d63842652f063f16c0827abda1922dfb9551a4ac23ba4462" origin="Generated by Gradle"/>
          </artifact>
@@ -621,8 +607,7 @@
             <sha256 value="96904172b35af353e4459786a7d02f1550698cd03b249799ecb563cea3b4c277" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-runtime" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-runtime" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-runtime-3.6.0.module">
             <sha256 value="3b99891842fdec80e7b24ae7f7c485ae41ca35b47c902ca2043cc948aaf58010" origin="Generated by Gradle"/>
          </artifact>
@@ -630,8 +615,7 @@
             <sha256 value="ac41d3f9b8a88046788c6827b0519bf0c53dcc271f598f48aa666c6f5a9523d0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-schema" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-schema" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-schema-3.6.0.module">
             <sha256 value="85abd765f2efca0545889c935d8c240e31736a22221231a59bcc4510358b6aaa" origin="Generated by Gradle"/>
          </artifact>
@@ -639,8 +623,7 @@
             <sha256 value="108bc4bafe7024a41460a1a60e72b6a95b69e5afd29c9f11ba7d8e0de2207976" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187 -->
-      <component group="de.undercouch" name="gradle-download-task" version="4.1.1">
+      <component group="de.undercouch" name="gradle-download-task" version="4.1.1" androidx:reason="Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187">
          <artifact name="gradle-download-task-4.1.1.jar">
             <ignored-keys>
                <ignored-key id="1fa37fbe4453c1073e7ef61d6449005f96bc97a3" reason="PGP verification failed"/>
@@ -658,8 +641,7 @@
             </sha256>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/johnrengelman/shadow/issues/760 -->
-      <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1">
+      <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1" androidx:reason="Unsigned https://github.com/johnrengelman/shadow/issues/760">
          <artifact name="shadow-7.1.1.jar">
             <sha256 value="a870861a7a3d54ffd97822051a27b2f1b86dd5c480317f0b97f3b27581b742af" origin="Generated by Gradle"/>
          </artifact>
@@ -667,8 +649,7 @@
             <sha256 value="683be0cd32af9c80a6d4a143b9a6ac2eb45ebc3ccd16db4ca11b94e55fc5e52f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13">
+      <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13" androidx:reason="Unsigned">
          <artifact name="protobuf-gradle-plugin-0.8.13.jar">
             <sha256 value="8a04b6eee4eab68c73b6e61cc8e00206753691b781d042afbae746f97e8c6f2d" origin="Generated by Gradle"/>
          </artifact>
@@ -676,8 +657,7 @@
             <sha256 value="d8c46016037cda6360561b9c6a21a6c2a4847cad15c3c63903e15328fbcccc45" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.activation" name="activation" version="1.1">
+      <component group="javax.activation" name="activation" version="1.1" androidx:reason="Unsigned">
          <artifact name="activation-1.1.jar">
             <sha256 value="2881c79c9d6ef01c58e62beea13e9d1ac8b8baa16f2fc198ad6e6776defdcdd3" origin="Generated by Gradle"/>
          </artifact>
@@ -685,8 +665,7 @@
             <sha256 value="d490e540a11504b9d71718b1c85fef7b3de6802361290824539b076d58faa8a0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.annotation" name="jsr250-api" version="1.0">
+      <component group="javax.annotation" name="jsr250-api" version="1.0" androidx:reason="Unsigned">
          <artifact name="jsr250-api-1.0.jar">
             <sha256 value="a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f" origin="Generated by Gradle"/>
          </artifact>
@@ -694,8 +673,7 @@
             <sha256 value="548b0ef6f04356ef2283af5140d9404f38fd3891a509d468537abf2f9462944d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.inject" name="javax.inject" version="1">
+      <component group="javax.inject" name="javax.inject" version="1" androidx:reason="Unsigned">
          <artifact name="javax.inject-1.jar">
             <sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/>
          </artifact>
@@ -703,8 +681,7 @@
             <sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.xml.stream" name="stax-api" version="1.0-2">
+      <component group="javax.xml.stream" name="stax-api" version="1.0-2" androidx:reason="Unsigned">
          <artifact name="stax-api-1.0-2.jar">
             <sha256 value="e8c70ebd76f982c9582a82ef82cf6ce14a7d58a4a4dca5cb7b7fc988c80089b7" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
@@ -712,8 +689,7 @@
             <sha256 value="2864f19da84fd52763d75a197a71779b2decbccaac3eb4e4760ffc884c5af4a2" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9">
+      <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9" androidx:reason="Unsigned">
          <artifact name="japicmp-gradle-plugin-0.2.9.jar">
             <sha256 value="320944e8f3a42a38a5e0f08c6e1e8ae11a63fc82e1f7bf0429a6b7d89d26fac3" origin="Generated by Gradle"/>
          </artifact>
@@ -721,8 +697,7 @@
             <sha256 value="41fc0c243907c241cffa24a06a8cb542747c848ebad5feb6b0413d61b4a0ebc2" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="nekohtml" name="nekohtml" version="1.9.6.2">
+      <component group="nekohtml" name="nekohtml" version="1.9.6.2" androidx:reason="Unsigned">
          <artifact name="nekohtml-1.9.6.2.jar">
             <sha256 value="fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09" origin="Generated by Gradle"/>
          </artifact>
@@ -730,8 +705,7 @@
             <sha256 value="f5655d331af6afcd4dbaedaa739b889380c771a7e83f7aea5c8544a05074cf0b" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="nekohtml" name="xercesMinimal" version="1.9.6.2">
+      <component group="nekohtml" name="xercesMinimal" version="1.9.6.2" androidx:reason="Unsigned">
          <artifact name="xercesMinimal-1.9.6.2.jar">
             <sha256 value="95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438" origin="Generated by Gradle"/>
          </artifact>
@@ -739,20 +713,17 @@
             <sha256 value="c219d697fa9c8f243d8f6e347499b6d4e8af1d0cac4bbc7b3907d338a2024c13" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="1">
+      <component group="net.java" name="jvnet-parent" version="1" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-1.pom">
             <sha256 value="281440811268e65d9e266b3cc898297e214e04f09740d0386ceeb4a8923d63bf" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="3">
+      <component group="net.java" name="jvnet-parent" version="3" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-3.pom">
             <sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="4">
+      <component group="net.java" name="jvnet-parent" version="4" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-4.pom">
             <sha256 value="471395735549495297c8ff939b9a32e08b91302020ff773586d27e497abb8fbb" origin="Generated by Gradle"/>
             <!-- Gradle doesn't add keyring files for parent poms so we need to explicitly specify it here to trust -->
@@ -760,14 +731,12 @@
             <pgp value="44fbdbbc1a00fe414f1c1873586654072ead6677"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="5">
+      <component group="net.java" name="jvnet-parent" version="5" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-5.pom">
             <sha256 value="1af699f8d9ddab67f9a0d202fbd7915eb0362a5a6dfd5ffc54cafa3465c9cb0a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.sf.kxml" name="kxml2" version="2.3.0">
+      <component group="net.sf.kxml" name="kxml2" version="2.3.0" androidx:reason="Unsigned">
          <artifact name="kxml2-2.3.0.jar">
             <sha256 value="f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2" origin="Generated by Gradle"/>
          </artifact>
@@ -775,8 +744,7 @@
             <sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2">
+      <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2" androidx:reason="Unsigned">
          <artifact name="tagsoup-1.2.jar">
             <sha256 value="10d12b82c9a58a7842765a1152a56fbbd11eac9122a621f5a86a087503297266" origin="Generated by Gradle"/>
          </artifact>
@@ -784,8 +752,7 @@
             <sha256 value="186fd460ee13150e31188703a2c871bf86e20332636f3ede4ab959cd5568da78" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15">
+      <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15" androidx:reason="Unsigned">
          <artifact name="plexus-utils-1.5.15.jar">
             <sha256 value="2ca121831e597b4d8f2cb22d17c5c041fc23a7777ceb6bfbdd4dfb34bbe7d997" origin="Generated by Gradle"/>
          </artifact>
@@ -793,26 +760,22 @@
             <sha256 value="12a3c9a32b82fdc95223cab1f9d344e14ef3e396da14c4d0013451646f3280e7" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus" version="1.0.4">
+      <component group="org.codehaus.plexus" name="plexus" version="1.0.4" androidx:reason="Unsigned">
          <artifact name="plexus-1.0.4.pom">
             <sha256 value="2242fd02d12b1ca73267fb3d89863025517200e7a4ee426cba4667d0172c74c3" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus" version="2.0.2">
+      <component group="org.codehaus.plexus" name="plexus" version="2.0.2" androidx:reason="Unsigned">
          <artifact name="plexus-2.0.2.pom">
             <sha256 value="e246e2a062b5d989fdefc521c9c56431ba5554ff8d2344edee9218a34a546a33" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14">
+      <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14" androidx:reason="Unsigned">
          <artifact name="plexus-components-1.1.14.pom">
             <sha256 value="381d72c526be217b770f9f8c3f749a86d3b1548ac5c1fcb48d267530ec60d43f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1">
+      <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1" androidx:reason="Unsigned">
          <artifact name="plexus-container-default-1.0-alpha-9-stable-1.jar">
             <sha256 value="7c758612888782ccfe376823aee7cdcc7e0cdafb097f7ef50295a0b0c3a16edf" origin="Generated by Gradle"/>
          </artifact>
@@ -820,14 +783,12 @@
             <sha256 value="ef71d45a49edfe76be0f520312a76bc2aae73ec0743a5ffffe10d30122c6e2b2" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3">
+      <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3" androidx:reason="Unsigned">
          <artifact name="plexus-containers-1.0.3.pom">
             <sha256 value="7c75075badcb014443ee94c8c4cad2f4a9905be3ce9430fe7b220afc7fa3a80f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11">
+      <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11" androidx:reason="Unsigned">
          <artifact name="plexus-interpolation-1.11.jar">
             <sha256 value="fd9507feb858fa620d1b4aa4b7039fdea1a77e09d3fd28cfbddfff468d9d8c28" origin="Generated by Gradle"/>
          </artifact>
@@ -835,8 +796,7 @@
             <sha256 value="b84d281f59b9da528139e0752a0e1cab0bd98d52c58442b00e45c9748e1d9eee" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-android-gradle-plugin-0.9.17-g014.jar">
             <sha256 value="64b2e96fd20762351c74f08d598d49c25a490a3b685b8a09446e81d6db36fe81" origin="Generated by Gradle"/>
          </artifact>
@@ -844,8 +804,7 @@
             <sha256 value="956ff381c6c775161a82823bb52d0aa40a8f6a37ab85059f149531f5e5efb7da" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-fatjar-0.9.17-g014.jar">
             <sha256 value="47cf09501402a101e555588cf5fa9ed83f8572bce9fd60db29e74b5d079628e3" origin="Generated by Gradle"/>
          </artifact>
@@ -853,8 +812,7 @@
             <sha256 value="ceb601f55f14337261fea474bb061407dc0e52146f80d74cd0b43d66febd401f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-gradle-plugin-0.9.17-g014.jar">
             <sha256 value="643a7eddeb521832c6021508b7477b603517438481bc06633dca12eb1f339422" origin="Generated by Gradle"/>
          </artifact>
@@ -867,8 +825,7 @@
             <sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm" version="7.0">
+      <component group="org.ow2.asm" name="asm" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-7.0.jar">
             <sha256 value="b88ef66468b3c978ad0c97fd6e90979e56155b4ac69089ba7a44e9aa7ffe9acf" origin="Generated by Gradle"/>
          </artifact>
@@ -876,8 +833,7 @@
             <sha256 value="83f65b1083d5ce4f8ba7f9545cfe9ff17824589c9a7cc82c3a4695801e4f5f68" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-analysis" version="7.0">
+      <component group="org.ow2.asm" name="asm-analysis" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-analysis-7.0.jar">
             <sha256 value="e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474" origin="Generated by Gradle"/>
          </artifact>
@@ -885,8 +841,7 @@
             <sha256 value="c6b54477e9d5bae1e7addff2e24cbf92aaff2ff08fd6bc0596c3933c3fadc2cb" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-commons" version="7.0">
+      <component group="org.ow2.asm" name="asm-commons" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-commons-7.0.jar">
             <sha256 value="fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d" origin="Generated by Gradle"/>
          </artifact>
@@ -894,8 +849,7 @@
             <sha256 value="f4c697886cdb4a5b2472054a0b5e34371e9b48e620be40c3ed48e1f4b6d51eb4" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-tree" version="7.0">
+      <component group="org.ow2.asm" name="asm-tree" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-tree-7.0.jar">
             <sha256 value="cfd7a0874f9de36a999c127feeadfbfe6e04d4a71ee954d7af3d853f0be48a6c" origin="Generated by Gradle"/>
          </artifact>
@@ -903,8 +857,7 @@
             <sha256 value="d39e7dd12f4ff535a0839d1949c39c7644355a4470220c94b76a5c168c57a068" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-util" version="7.0">
+      <component group="org.ow2.asm" name="asm-util" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-util-7.0.jar">
             <sha256 value="75fbbca440ef463f41c2b0ab1a80abe67e910ac486da60a7863cbcb5bae7e145" origin="Generated by Gradle"/>
          </artifact>
@@ -912,14 +865,12 @@
             <sha256 value="e07bce4bb55d5a06f4c10d912fc9dee8a9b9c04ec549bbb8db4f20db34706f75" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.sonatype.oss" name="oss-parent" version="7">
+      <component group="org.sonatype.oss" name="oss-parent" version="7" androidx:reason="Unsigned">
          <artifact name="oss-parent-7.pom">
             <sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Invalid signature -->
-      <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2">
+      <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2" androidx:reason="Invalid signature">
          <artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar">
             <pgp value="db0597e3144342256bc81e3ec727d053c4481cf5"/>
             <sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
@@ -933,8 +884,7 @@
             </sha256>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="pull-parser" name="pull-parser" version="2">
+      <component group="pull-parser" name="pull-parser" version="2" androidx:reason="Unsigned">
          <artifact name="pull-parser-2.jar">
             <sha256 value="b20c1e56513faeffb9b01d9d03ba1a36128ac3f9be39b2d0edbe2e240b029d3f" origin="Generated by Gradle"/>
          </artifact>
@@ -942,8 +892,7 @@
             <sha256 value="4823677670797c2b71e8ebbe5437c41151f4e8edb7c6c0d473279715070f36d3" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="xmlpull" name="xmlpull" version="1.1.3.1">
+      <component group="xmlpull" name="xmlpull" version="1.1.3.1" androidx:reason="Unsigned">
          <artifact name="xmlpull-1.1.3.1.jar">
             <sha256 value="34e08ee62116071cbb69c0ed70d15a7a5b208d62798c59f2120bb8929324cb63" origin="Generated by Gradle"/>
          </artifact>
@@ -951,8 +900,7 @@
             <sha256 value="8f10ffd8df0d3e9819c8cc8402709c6b248bc53a954ef6e45470d9ae3a5735fb" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="xpp3" name="xpp3" version="1.1.4c">
+      <component group="xpp3" name="xpp3" version="1.1.4c" androidx:reason="Unsigned">
          <artifact name="xpp3-1.1.4c.jar">
             <sha256 value="0341395a481bb887803957145a6a37879853dd625e9244c2ea2509d9bb7531b9" origin="Generated by Gradle"/>
          </artifact>
@@ -960,8 +908,7 @@
             <sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned, b/227204920 -->
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10">
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10" androidx:reason="Unsigned, b/227204920">
          <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz">
             <sha256 value="f3bd13bc0089fe95609109604d5993a49838828787f15e0e79eef6612b587dc1" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz"/>
          </artifact>
diff --git a/libraryversions.toml b/libraryversions.toml
index 6fe069f..1f75405 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,7 +1,7 @@
 [versions]
 ACTIVITY = "1.7.0-alpha01"
 ADS_IDENTIFIER = "1.0.0-alpha05"
-ANNOTATION = "1.4.0-rc01"
+ANNOTATION = "1.5.0-alpha01"
 ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
 APPCOMPAT = "1.5.0-rc01"
 APPSEARCH = "1.0.0-alpha05"
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c872393..8bc8969 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -32,10 +32,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.savedstate:savedstate-ktx:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
 
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index f26a72d..3a2b25f 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -33,13 +33,13 @@
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.0"
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 9a64a90..12ced4a 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    api("androidx.fragment:fragment-ktx:1.5.0")
+    api("androidx.fragment:fragment-ktx:1.5.1")
     api(project(":navigation:navigation-runtime"))
     api("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
     api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 6478105..70c2046 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,8 +26,8 @@
 dependencies {
     api(project(":navigation:navigation-common"))
     api("androidx.activity:activity-ktx:1.5.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index fdc7e4b..8f4aacb5 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -23,6 +23,11 @@
 }
 
 dependencies {
+    // Atomic Group
+    constraints {
+        implementation(project(":paging:paging-runtime"))
+    }
+
     api("androidx.annotation:annotation:1.3.0")
     api("androidx.arch.core:core-common:2.1.0")
     api(libs.kotlinStdlib)
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e7cc847..eb2de3b 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -31,6 +31,11 @@
 }
 
 dependencies {
+    //Atomic Group
+    constraints {
+        implementation(project(":paging:paging-common"))
+    }
+
     api(project(":paging:paging-common"))
     // Ensure that the -ktx dependency graph mirrors the Java dependency graph
     api(project(":paging:paging-common-ktx"))
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index e7cf93d..afbb5e3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -19,6 +19,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.database.sqlite.SQLiteConstraintException;
 import android.database.sqlite.SQLiteException;
 
 import androidx.annotation.NonNull;
@@ -85,7 +86,7 @@
                     3,
                     true
             );
-        } catch (IllegalStateException e) {
+        } catch (SQLiteConstraintException e) {
             assertThat(e.getMessage()).isEqualTo("Foreign key violation(s) detected in 'Entity9'."
                     + "\nNumber of different violations discovered: 1"
                     + "\nNumber of rows in violation: 2"
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
index a5ba88e..1e2df59 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
@@ -50,6 +50,7 @@
 
 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;
@@ -154,6 +155,7 @@
         stopObserver(user1, observer);
     }
 
+    @Ignore("b/239575607")
     @Test
     public void parallelWrites() throws InterruptedException, ExecutionException {
         int numberOfThreads = 10;
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
index 038f117..db24932 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
@@ -26,15 +26,9 @@
         val existingFieldNames = mutableSetOf<String>()
         suspend fun SequenceScope<XFieldElement>.yieldAllFields(type: XTypeElement) {
             // yield all fields declared directly on this type
-            type.getDeclaredFields().forEach {
-                if (existingFieldNames.add(it.name)) {
-                    if (type == xTypeElement) {
-                        yield(it)
-                    } else {
-                        yield(it.copyTo(xTypeElement))
-                    }
-                }
-            }
+            type.getDeclaredFields()
+                .filter { existingFieldNames.add(it.name) }
+                .forEach { yield(it) }
             // visit all declared fields on super types
             type.superClass?.typeElement?.let { parent ->
                 yieldAllFields(parent)
@@ -78,7 +72,6 @@
                 type.getDeclaredMethods()
                     .filter { it.isAccessibleFrom(xTypeElement.packageName) }
                     .filterNot { it.isStaticInterfaceMethod() }
-                    .map { it.copyTo(xTypeElement) }
                     .forEach {
                         methodsByName.getOrPut(it.name) { linkedSetOf() }.add(it)
                     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
index 19fa7ef..0a7095d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
@@ -38,9 +38,4 @@
 
     override val fallbackLocationText: String
         get() = "$name in ${enclosingElement.fallbackLocationText}"
-
-    /**
-     * Creates a new [XFieldElement] where containing element is replaced with [newContainer].
-     */
-    fun copyTo(newContainer: XTypeElement): XFieldElement
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
index a023f0d..96fcfc5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
@@ -117,11 +117,6 @@
     fun overrides(other: XMethodElement, owner: XTypeElement): Boolean
 
     /**
-     * Creates a new [XMethodElement] where containing element is replaced with [newContainer].
-     */
-    fun copyTo(newContainer: XTypeElement): XMethodElement
-
-    /**
      * If true, this method can be invoked from Java sources. This is especially important for
      * Kotlin functions that receive value class as a parameter as they cannot be called from Java
      * sources.
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
index ef6c920..135970e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
@@ -27,13 +27,8 @@
 
 internal class JavacConstructorElement(
     env: JavacProcessingEnv,
-    containing: JavacTypeElement,
     element: ExecutableElement
-) : JavacExecutableElement(
-    env,
-    containing,
-    element
-),
+) : JavacExecutableElement(env, element),
     XConstructorElement {
     init {
         check(element.kind == ElementKind.CONSTRUCTOR) {
@@ -56,7 +51,6 @@
             JavacMethodParameter(
                 env = env,
                 enclosingElement = this,
-                containing = containing,
                 element = variable,
                 kotlinMetadataFactory = { kotlinMetadata?.parameters?.getOrNull(index) },
                 argIndex = index
@@ -65,16 +59,15 @@
     }
 
     override val executableType: XConstructorType by lazy {
-        val asMemberOf = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
         JavacConstructorType(
             env = env,
             element = this,
-            executableType = MoreTypes.asExecutable(asMemberOf)
+            executableType = MoreTypes.asExecutable(element.asType())
         )
     }
 
     override fun asMemberOf(other: XType): XConstructorType {
-        return if (other !is JavacDeclaredType || containing.type.isSameType(other)) {
+        return if (other !is JavacDeclaredType || enclosingElement.type.isSameType(other)) {
             executableType
         } else {
             val asMemberOf = env.typeUtils.asMemberOf(other.typeMirror, element)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index ff96b7e..e02045a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -79,6 +79,10 @@
         return element.toString()
     }
 
+    final override val equalityItems: Array<out Any?> by lazy {
+        arrayOf(element)
+    }
+
     override fun equals(other: Any?): Boolean {
         return XEquality.equals(this, other)
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
index afee3e4..8a9469e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
@@ -29,10 +29,6 @@
     override val name: String
         get() = element.simpleName.toString()
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(name, enclosingElement)
-    }
-
     override val fallbackLocationText: String
         get() = "$name enum entry in ${enclosingElement.fallbackLocationText}"
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
index 2583b9b..5dc5b2e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
@@ -25,12 +25,8 @@
 
 internal abstract class JavacExecutableElement(
     env: JavacProcessingEnv,
-    val containing: JavacTypeElement,
     override val element: ExecutableElement
-) : JavacElement(
-    env,
-    element
-),
+) : JavacElement(env, element),
     XExecutableElement,
     XHasModifiers by JavacHasModifiers(element) {
     abstract val kotlinMetadata: KmExecutable?
@@ -41,10 +37,6 @@
 
     abstract override val parameters: List<JavacMethodParameter>
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(element, containing)
-    }
-
     override val enclosingElement: JavacTypeElement by lazy {
         element.requireEnclosingType(env)
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
index 1cefc90..bc8dbd7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
@@ -18,16 +18,14 @@
 
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
-import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.javac.kotlin.KmProperty
 import androidx.room.compiler.processing.javac.kotlin.KmType
 import javax.lang.model.element.VariableElement
 
 internal class JavacFieldElement(
     env: JavacProcessingEnv,
-    containing: JavacTypeElement,
     element: VariableElement
-) : JavacVariableElement(env, containing, element),
+) : JavacVariableElement(env, element),
     XFieldElement,
     XHasModifiers by JavacHasModifiers(element) {
 
@@ -44,15 +42,4 @@
 
     override val closestMemberContainer: JavacTypeElement
         get() = enclosingElement
-
-    override fun copyTo(newContainer: XTypeElement): JavacFieldElement {
-        check(newContainer is JavacTypeElement) {
-            "Unexpected container (${newContainer::class}), expected JavacTypeElement"
-        }
-        return JavacFieldElement(
-            env = env,
-            containing = newContainer,
-            element = element
-        )
-    }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
index c06ca00..68b8097 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
@@ -33,13 +33,8 @@
 
 internal class JavacMethodElement(
     env: JavacProcessingEnv,
-    containing: JavacTypeElement,
     element: ExecutableElement
-) : JavacExecutableElement(
-    env,
-    containing,
-    element
-),
+) : JavacExecutableElement(env, element),
     XMethodElement {
     init {
         check(element.kind == ElementKind.METHOD) {
@@ -66,7 +61,6 @@
             JavacMethodParameter(
                 env = env,
                 enclosingElement = this,
-                containing = containing,
                 element = variable,
                 kotlinMetadataFactory = {
                     val metadataParamIndex = if (isExtensionFunction()) index - 1 else index
@@ -82,19 +76,16 @@
     }
 
     override val executableType: JavacMethodType by lazy {
-        val asMemberOf = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
         JavacMethodType.create(
             env = env,
             element = this,
-            executableType = MoreTypes.asExecutable(asMemberOf)
+            executableType = MoreTypes.asExecutable(element.asType())
         )
     }
 
     override val returnType: JavacType by lazy {
-        val asMember = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
-        val asExec = MoreTypes.asExecutable(asMember)
-        env.wrap<JavacType>(
-            typeMirror = asExec.returnType,
+        env.wrap(
+            typeMirror = element.returnType,
             kotlinType = if (isSuspendFunction()) {
                 // Don't use Kotlin metadata for suspend functions since we want the Java
                 // perspective. In Java, a suspend function returns Object and contains an extra
@@ -109,7 +100,7 @@
     }
 
     override fun asMemberOf(other: XType): XMethodType {
-        return if (other !is JavacDeclaredType || containing.type.isSameType(other)) {
+        return if (other !is JavacDeclaredType || enclosingElement.type.isSameType(other)) {
             executableType
         } else {
             val asMemberOf = env.typeUtils.asMemberOf(other.typeMirror, element)
@@ -142,15 +133,6 @@
         return MoreElements.overrides(element, other.element, owner.element, env.typeUtils)
     }
 
-    override fun copyTo(newContainer: XTypeElement): XMethodElement {
-        check(newContainer is JavacTypeElement)
-        return JavacMethodElement(
-            env = env,
-            containing = newContainer,
-            element = element
-        )
-    }
-
     override fun hasKotlinDefaultImpl(): Boolean {
         fun paramsMatch(
             ourParams: List<JavacMethodParameter>,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
index b6a4163..7288af9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
@@ -26,11 +26,10 @@
 internal class JavacMethodParameter(
     env: JavacProcessingEnv,
     override val enclosingElement: JavacExecutableElement,
-    containing: JavacTypeElement,
     element: VariableElement,
     kotlinMetadataFactory: () -> KmValueParameter?,
     val argIndex: Int
-) : JavacVariableElement(env, containing, element), XExecutableParameterElement {
+) : JavacVariableElement(env, element), XExecutableParameterElement {
 
     private val kotlinMetadata by lazy { kotlinMetadataFactory() }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 25912f6..0d0a3b0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -259,20 +259,16 @@
     }
 
     fun wrapExecutableElement(element: ExecutableElement): JavacExecutableElement {
-        val enclosingType = element.requireEnclosingType(this)
-
         return when (element.kind) {
             ElementKind.CONSTRUCTOR -> {
                 JavacConstructorElement(
                     env = this,
-                    containing = enclosingType,
                     element = element
                 )
             }
             ElementKind.METHOD -> {
                 JavacMethodElement(
                     env = this,
-                    containing = enclosingType,
                     element = element
                 )
             }
@@ -288,9 +284,7 @@
                     param.element.simpleName == element.simpleName
                 } ?: error("Unable to create variable element for $element")
             }
-            is TypeElement -> {
-                JavacFieldElement(this, wrapTypeElement(enclosingElement), element)
-            }
+            is TypeElement -> JavacFieldElement(this, element)
             else -> error("Unsupported enclosing type $enclosingElement for $element")
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 4911f16..0eb5474 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -88,7 +88,6 @@
                 JavacFieldElement(
                     env = env,
                     element = it,
-                    containing = this
                 )
             }
     }
@@ -145,7 +144,6 @@
         ElementFilter.methodsIn(element.enclosedElements).map {
             JavacMethodElement(
                 env = env,
-                containing = this,
                 element = it
             )
         }.filterMethodsByConfig(env)
@@ -159,7 +157,6 @@
         return ElementFilter.constructorsIn(element.enclosedElements).map {
             JavacConstructorElement(
                 env = env,
-                containing = this,
                 element = it
             )
         }
@@ -178,7 +175,7 @@
     }
 
     override val type: JavacDeclaredType by lazy {
-        env.wrap<JavacDeclaredType>(
+        env.wrap(
             typeMirror = element.asType(),
             kotlinType = kotlinMetadata?.kmType,
             elementNullability = element.nullability
@@ -225,10 +222,6 @@
         }
     }
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(element)
-    }
-
     class DefaultJavacTypeElement(
         env: JavacProcessingEnv,
         element: TypeElement
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
index 42d85fb..fdf8bb3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
@@ -47,7 +47,4 @@
 
     override val closestMemberContainer: XMemberContainer
         get() = enclosingElement.closestMemberContainer
-
-    override val equalityItems: Array<out Any?>
-        get() = arrayOf(element)
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
index e999b14..daec9e9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
@@ -24,7 +24,6 @@
 
 internal abstract class JavacVariableElement(
     env: JavacProcessingEnv,
-    val containing: JavacTypeElement,
     override val element: VariableElement
 ) : JavacElement(env, element), XVariableElement {
 
@@ -34,30 +33,24 @@
         get() = element.simpleName.toString()
 
     override val type: JavacType by lazy {
-        MoreTypes.asMemberOf(env.typeUtils, containing.type.typeMirror, element).let {
-            env.wrap<JavacType>(
-                typeMirror = it,
-                kotlinType = kotlinType,
-                elementNullability = element.nullability
-            )
-        }
+        env.wrap(
+            typeMirror = element.asType(),
+            kotlinType = kotlinType,
+            elementNullability = element.nullability
+        )
     }
 
     override fun asMemberOf(other: XType): JavacType {
-        return if (containing.type.isSameType(other)) {
+        return if (closestMemberContainer.type?.isSameType(other) == true) {
             type
         } else {
             check(other is JavacDeclaredType)
             val asMember = MoreTypes.asMemberOf(env.typeUtils, other.typeMirror, element)
-            env.wrap<JavacType>(
+            env.wrap(
                 typeMirror = asMember,
                 kotlinType = kotlinType,
                 elementNullability = element.nullability
             )
         }
     }
-
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(element, containing)
-    }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
index 614d19c..a452ced 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
@@ -24,14 +24,8 @@
 
 internal class KspConstructorElement(
     env: KspProcessingEnv,
-    override val containing: KspTypeElement,
     declaration: KSFunctionDeclaration
-) : KspExecutableElement(
-    env = env,
-    containing = containing,
-    declaration = declaration
-),
-    XConstructorElement {
+) : KspExecutableElement(env, declaration), XConstructorElement {
     override val name: String
         get() = "<init>"
 
@@ -55,7 +49,7 @@
         KspConstructorType(
             env = env,
             origin = this,
-            containing = this.containing.type
+            containing = this.enclosingElement.type
         )
     }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
index 3f63842..077063f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
@@ -41,6 +41,10 @@
         }
     }
 
+    final override val equalityItems: Array<out Any?> by lazy {
+        arrayOf(declaration)
+    }
+
     override fun equals(other: Any?): Boolean {
         return XEquality.equals(this, other)
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index ac41dfe..818856d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -31,12 +31,8 @@
 
 internal abstract class KspExecutableElement(
     env: KspProcessingEnv,
-    open val containing: KspMemberContainer,
     override val declaration: KSFunctionDeclaration
-) : KspElement(
-    env = env,
-    declaration = declaration
-),
+) : KspElement(env, declaration),
     XExecutableElement,
     XHasModifiers by KspHasModifiers.create(declaration),
     XAnnotated by KspAnnotated.create(
@@ -45,10 +41,6 @@
         filter = NO_USE_SITE
     ) {
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(containing, declaration)
-    }
-
     override val enclosingElement: KspMemberContainer by lazy {
         declaration.requireEnclosingMemberContainer(env)
     }
@@ -94,22 +86,8 @@
             }
 
             return when {
-                declaration.isConstructor() -> {
-                    KspConstructorElement(
-                        env = env,
-                        containing = enclosingContainer as? KspTypeElement ?: error(
-                            "The container for $declaration should be a type element"
-                        ),
-                        declaration = declaration
-                    )
-                }
-                else -> {
-                    KspMethodElement.create(
-                        env = env,
-                        containing = enclosingContainer,
-                        declaration = declaration
-                    )
-                }
+                declaration.isConstructor() -> KspConstructorElement(env, declaration)
+                else -> KspMethodElement.create(env, declaration)
             }
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 135ef76..8a6ed9a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -22,8 +22,10 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_METHOD_PARAMETER
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
+import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
 import com.google.devtools.ksp.symbol.KSPropertySetter
+import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSValueParameter
 
 internal class KspExecutableParameterElement(
@@ -35,33 +37,23 @@
     XExecutableParameterElement,
     XAnnotated by KspAnnotated.create(env, parameter, NO_USE_SITE_OR_METHOD_PARAMETER) {
 
-    override val equalityItems: Array<out Any?>
-        get() = arrayOf(enclosingElement, parameter)
-
     override val name: String
         get() = parameter.name?.asString() ?: "_no_param_name"
 
     override val hasDefaultValue: Boolean
         get() = parameter.hasDefault
 
-    private val jvmTypeResolver by lazy {
-        KspJvmTypeResolutionScope.MethodParameter(
+    private fun jvmTypeResolver(container: KSDeclaration?): KspJvmTypeResolutionScope {
+        return KspJvmTypeResolutionScope.MethodParameter(
             kspExecutableElement = enclosingElement,
             parameterIndex = parameterIndex,
-            annotated = parameter.type
+            annotated = parameter.type,
+            container = container
         )
     }
 
     override val type: KspType by lazy {
-        parameter.typeAsMemberOf(
-            functionDeclaration = enclosingElement.declaration,
-            ksType = enclosingElement.containing.type?.ksType
-        ).let {
-            env.wrap(
-                originatingReference = parameter.type,
-                ksType = it
-            ).withJvmTypeResolver(jvmTypeResolver)
-        }
+        asMemberOf(enclosingElement.enclosingElement.type?.ksType)
     }
 
     override val closestMemberContainer: XMemberContainer by lazy {
@@ -72,19 +64,25 @@
         get() = "$name in ${enclosingElement.fallbackLocationText}"
 
     override fun asMemberOf(other: XType): KspType {
-        if (enclosingElement.containing.type?.isSameType(other) != false) {
+        if (closestMemberContainer.type?.isSameType(other) != false) {
             return type
         }
         check(other is KspType)
-        return parameter.typeAsMemberOf(
-            functionDeclaration = enclosingElement.declaration,
-            ksType = other.ksType
-        ).let {
-            env.wrap(
-                originatingReference = parameter.type,
-                ksType = it
-            ).withJvmTypeResolver(jvmTypeResolver)
-        }
+        return asMemberOf(other.ksType)
+    }
+
+    private fun asMemberOf(ksType: KSType?): KspType {
+        return env.wrap(
+            originatingReference = parameter.type,
+            ksType = parameter.typeAsMemberOf(
+                functionDeclaration = enclosingElement.declaration,
+                ksType = ksType
+            )
+        ).withJvmTypeResolver(
+            jvmTypeResolver(
+                container = ksType?.declaration
+            )
+        )
     }
 
     override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
index dafbc4d..8f8b31d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
@@ -20,27 +20,21 @@
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_FIELD
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
-import com.google.devtools.ksp.closestClassDeclaration
 import com.google.devtools.ksp.isPrivate
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
+import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.Modifier
 
 internal class KspFieldElement(
     env: KspProcessingEnv,
     override val declaration: KSPropertyDeclaration,
-    val containing: KspMemberContainer
 ) : KspElement(env, declaration),
     XFieldElement,
     XHasModifiers by KspHasModifiers.create(declaration),
     XAnnotated by KspAnnotated.create(env, declaration, NO_USE_SITE_OR_FIELD) {
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(declaration, containing)
-    }
-
     override val enclosingElement: KspMemberContainer by lazy {
         declaration.requireEnclosingMemberContainer(env)
     }
@@ -54,29 +48,7 @@
     }
 
     override val type: KspType by lazy {
-        env.wrap(
-            originatingReference = declaration.type,
-            ksType = declaration.typeAsMemberOf(containing.type?.ksType)
-        )
-    }
-
-    /**
-     * The original field from the declaration. For instance, if you have `val x:String` declared
-     * in `BaseClass` and inherited in `SubClass`, if `this` is the instance in `SubClass`,
-     * [declarationField] will be the instance in `BaseClass`. If `this` is the instance in
-     * `BaseClass`, [declarationField] will be `null`.
-     */
-    val declarationField: KspFieldElement? by lazy {
-        val declaredIn = declaration.closestClassDeclaration()
-        if (declaredIn == null || declaredIn == containing.declaration) {
-            null
-        } else {
-            KspFieldElement(
-                env = env,
-                declaration = declaration,
-                containing = env.wrapClassDeclaration(declaredIn)
-            )
-        }
+        asMemberOf(enclosingElement.type?.ksType)
     }
 
     val syntheticAccessors: List<KspSyntheticPropertyMethodElement> by lazy {
@@ -123,38 +95,23 @@
         }
 
     override fun asMemberOf(other: XType): KspType {
-        if (containing.type?.isSameType(other) != false) {
+        if (enclosingElement.type?.isSameType(other) != false) {
             return type
         }
         check(other is KspType)
-        val asMember = declaration.typeAsMemberOf(other.ksType)
-        return env.wrap(
-            originatingReference = declaration.type,
-            ksType = asMember
-        )
+        return asMemberOf(other.ksType)
     }
 
-    override fun copyTo(newContainer: XTypeElement): KspFieldElement {
-        check(newContainer is KspTypeElement) {
-            "Unexpected container (${newContainer::class}), expected KspTypeElement"
-        }
-        return KspFieldElement(
-            env = env,
-            declaration = declaration,
-            containing = newContainer
+    private fun asMemberOf(ksType: KSType?): KspType {
+        return env.wrap(
+            originatingReference = declaration.type,
+            ksType = declaration.typeAsMemberOf(ksType)
         )
     }
 
     companion object {
-        fun create(
-            env: KspProcessingEnv,
-            declaration: KSPropertyDeclaration
-        ): KspFieldElement {
-            return KspFieldElement(
-                env = env,
-                declaration = declaration,
-                containing = declaration.requireEnclosingMemberContainer(env)
-            )
+        fun create(env: KspProcessingEnv, declaration: KSPropertyDeclaration): KspFieldElement {
+            return KspFieldElement(env, declaration)
         }
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
index 489f6fa..eda347f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
@@ -114,17 +114,14 @@
         private val kspExecutableElement: KspExecutableElement,
         private val parameterIndex: Int,
         annotated: KSAnnotated,
-    ) : KspJvmTypeResolutionScope(
-        annotated = annotated,
-        container = kspExecutableElement.containing.declaration
-    ) {
+        container: KSDeclaration?,
+    ) : KspJvmTypeResolutionScope(annotated, container) {
         override fun findDeclarationType(): XType? {
-            val declarationMethodType = if (kspExecutableElement is KspMethodElement) {
-                kspExecutableElement.declarationMethodType
+            return if (kspExecutableElement is KspMethodElement) {
+                kspExecutableElement.executableType.parameterTypes.getOrNull(parameterIndex)
             } else {
                 null
             }
-            return declarationMethodType?.parameterTypes?.getOrNull(parameterIndex)
         }
     }
 
@@ -132,13 +129,12 @@
         val declaration: KspSyntheticPropertyMethodElement
     ) : KspJvmTypeResolutionScope(
         annotated = declaration.accessor,
-        container = declaration.field.containing.declaration
+        container = declaration.field.enclosingElement.declaration
     ) {
         override fun findDeclarationType(): XType? {
             // We return the declaration from the setter, not the field because the setter parameter
             // will have a different type in jvm (due to jvm wildcard resolution)
-            return declaration.field.declarationField
-                ?.syntheticSetter?.parameters?.firstOrNull()?.type
+            return declaration.field.syntheticSetter?.parameters?.firstOrNull()?.type
         }
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index b833951..761a615 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -16,7 +16,6 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.compiler.processing.XExecutableParameterElement
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XMethodType
@@ -25,7 +24,6 @@
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticContinuationParameterElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticReceiverParameterElement
 import com.google.devtools.ksp.KspExperimental
-import com.google.devtools.ksp.closestClassDeclaration
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
@@ -33,14 +31,8 @@
 
 internal sealed class KspMethodElement(
     env: KspProcessingEnv,
-    containing: KspMemberContainer,
     declaration: KSFunctionDeclaration
-) : KspExecutableElement(
-    env = env,
-    containing = containing,
-    declaration = declaration
-),
-    XMethodElement {
+) : KspExecutableElement(env, declaration), XMethodElement {
 
     override val name: String
         get() = declaration.simpleName.asString()
@@ -55,7 +47,7 @@
     }
 
     override val parameters: List<XExecutableParameterElement> by lazy {
-        buildList<XExecutableParameterElement> {
+        buildList {
             val extensionReceiver = declaration.extensionReceiver
             if (extensionReceiver != null) {
                 // Synthesize the receiver parameter to be consistent with KAPT
@@ -84,38 +76,10 @@
         KspMethodType.create(
             env = env,
             origin = this,
-            containing = this.containing.type
+            containing = this.enclosingElement.type
         )
     }
 
-    /**
-     * The method type for the declaration if it is inherited from a super.
-     * If this method is declared in the containing class (or in a file), it will be null.
-     */
-    val declarationMethodType: XMethodType? by lazy {
-        declaration.closestClassDeclaration()?.let { declaredIn ->
-            if (declaredIn == containing.declaration) {
-                executableType
-            } else {
-                create(
-                    env = env,
-                    containing = env.wrapClassDeclaration(declaredIn),
-                    declaration = declaration
-                ).executableType
-            }
-        }
-    }
-
-    override val enclosingElement: KspMemberContainer
-        // KSFunctionDeclarationJavaImpl.parent returns null for generated static enum functions
-        // `values` and `valueOf` in Java source(https://github.com/google/ksp/issues/816).
-        // To bypass this we use `containing` for these functions.
-        get() = if (containing is XEnumTypeElement && (name == "values" || name == "valueOf")) {
-            containing
-        } else {
-            super.enclosingElement
-        }
-
     override fun isJavaDefault(): Boolean {
         return declaration.modifiers.contains(Modifier.JAVA_DEFAULT) ||
             declaration.hasJvmDefaultAnnotation()
@@ -146,26 +110,14 @@
         return env.resolver.overrides(this, other)
     }
 
-    override fun copyTo(newContainer: XTypeElement): KspMethodElement {
-        check(newContainer is KspTypeElement)
-        return create(
-            env = env,
-            containing = newContainer,
-            declaration = declaration
-        )
-    }
-
     private class KspNormalMethodElement(
         env: KspProcessingEnv,
-        containing: KspMemberContainer,
         declaration: KSFunctionDeclaration
-    ) : KspMethodElement(
-        env, containing, declaration
-    ) {
+    ) : KspMethodElement(env, declaration) {
         override val returnType: XType by lazy {
             declaration.returnKspType(
                 env = env,
-                containing = containing.type
+                containing = enclosingElement.type
             )
         }
         override fun isSuspendFunction() = false
@@ -173,11 +125,8 @@
 
     private class KspSuspendMethodElement(
         env: KspProcessingEnv,
-        containing: KspMemberContainer,
         declaration: KSFunctionDeclaration
-    ) : KspMethodElement(
-        env, containing, declaration
-    ) {
+    ) : KspMethodElement(env, declaration) {
         override fun isSuspendFunction() = true
 
         override val returnType: XType by lazy {
@@ -197,13 +146,12 @@
     companion object {
         fun create(
             env: KspProcessingEnv,
-            containing: KspMemberContainer,
             declaration: KSFunctionDeclaration
         ): KspMethodElement {
             return if (declaration.modifiers.contains(Modifier.SUSPEND)) {
-                KspSuspendMethodElement(env, containing, declaration)
+                KspSuspendMethodElement(env, declaration)
             } else {
-                KspNormalMethodElement(env, containing, declaration)
+                KspNormalMethodElement(env, declaration)
             }
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 94d35a7..7366726 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -74,10 +74,6 @@
         declaration.typeParameters.map { KspTypeParameterElement(env, it) }
     }
 
-    override val equalityItems: Array<out Any?> by lazy {
-        arrayOf(declaration)
-    }
-
     override val qualifiedName: String by lazy {
         (declaration.qualifiedName ?: declaration.simpleName).asString()
     }
@@ -164,7 +160,6 @@
                 KspFieldElement(
                     env = env,
                     declaration = it,
-                    containing = this
                 )
             }.let {
                 // only order instance properties with backing fields, we don't care about the order
@@ -185,7 +180,6 @@
                 KspFieldElement(
                     env = env,
                     declaration = it,
-                    containing = this
                 )
             }
         declaredProperties + companionProperties
@@ -256,7 +250,6 @@
         return declaration.primaryConstructor?.let {
             KspConstructorElement(
                 env = env,
-                containing = this,
                 declaration = it
             )
         }
@@ -280,7 +273,6 @@
             }.map {
                 KspMethodElement.create(
                     env = env,
-                    containing = this,
                     declaration = it
                 )
             }.toList()
@@ -297,7 +289,6 @@
         return declaration.getConstructors().map {
             KspConstructorElement(
                 env = env,
-                containing = this,
                 declaration = it
             )
         }.toList()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
index 8d9f24d6..fe3f77e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
@@ -53,7 +53,4 @@
 
     override val closestMemberContainer: XMemberContainer
         get() = enclosingElement.closestMemberContainer
-
-    override val equalityItems: Array<out Any?>
-        get() = arrayOf(declaration)
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 980fda2..9fe597a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -30,6 +30,8 @@
 import androidx.room.compiler.processing.ksp.requireContinuationClass
 import androidx.room.compiler.processing.ksp.returnTypeAsMemberOf
 import androidx.room.compiler.processing.ksp.swapResolvedType
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.Variance
 
 /**
@@ -67,37 +69,17 @@
     override val hasDefaultValue: Boolean
         get() = false
 
-    private val jvmTypeResolutionScope by lazy {
-        KspJvmTypeResolutionScope.MethodParameter(
+    private fun jvmTypeResolutionScope(container: KSDeclaration?): KspJvmTypeResolutionScope {
+        return KspJvmTypeResolutionScope.MethodParameter(
             kspExecutableElement = enclosingElement,
             parameterIndex = enclosingElement.parameters.size - 1,
-            annotated = enclosingElement.declaration
+            annotated = enclosingElement.declaration,
+            container = container
         )
     }
 
-    override val type: XType by lazy {
-        val continuation = env.resolver.requireContinuationClass()
-        val asMember = enclosingElement.declaration.returnTypeAsMemberOf(
-            ksType = enclosingElement.containing.type?.ksType
-        )
-        val returnTypeRef = checkNotNull(enclosingElement.declaration.returnType) {
-            "cannot find return type reference for $this"
-        }
-        val returnTypeAsTypeArgument = env.resolver.getTypeArgument(
-            returnTypeRef.swapResolvedType(asMember),
-            // even though this will be CONTRAVARIANT when resolved to the JVM type, in Kotlin, it
-            // is still INVARIANT. (see [KSTypeVarianceResolver]
-            Variance.INVARIANT
-        )
-        val contType = continuation.asType(
-            listOf(
-                returnTypeAsTypeArgument
-            )
-        )
-        env.wrap(
-            ksType = contType,
-            allowPrimitives = false
-        ).withJvmTypeResolver(jvmTypeResolutionScope)
+    override val type: KspType by lazy {
+        asMemberOf(enclosingElement.enclosingElement.type?.ksType)
     }
 
     override val fallbackLocationText: String
@@ -111,10 +93,17 @@
     }
 
     override fun asMemberOf(other: XType): KspType {
+        if (enclosingElement.enclosingElement.type?.isSameType(other) != false) {
+            return type
+        }
         check(other is KspType)
+        return asMemberOf(other.ksType)
+    }
+
+    private fun asMemberOf(ksType: KSType?): KspType {
         val continuation = env.resolver.requireContinuationClass()
         val asMember = enclosingElement.declaration.returnTypeAsMemberOf(
-            ksType = other.ksType
+            ksType = ksType
         )
         val returnTypeRef = checkNotNull(enclosingElement.declaration.returnType) {
             "cannot find return type reference for $this"
@@ -130,7 +119,9 @@
             ksType = contType,
             allowPrimitives = false
         ).withJvmTypeResolver(
-            jvmTypeResolutionScope
+            jvmTypeResolutionScope(
+                container = ksType?.declaration
+            )
         )
     }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 3b2dbd0..af58c59 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -35,7 +35,6 @@
 import androidx.room.compiler.processing.ksp.KspHasModifiers
 import androidx.room.compiler.processing.ksp.KspJvmTypeResolutionScope
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
-import androidx.room.compiler.processing.ksp.KspTypeElement
 import androidx.room.compiler.processing.ksp.KspType
 import androidx.room.compiler.processing.ksp.findEnclosingMemberContainer
 import androidx.room.compiler.processing.ksp.overrides
@@ -96,7 +95,7 @@
     final override val executableType: XMethodType by lazy {
         KspSyntheticPropertyMethodType.create(
             element = this,
-            container = field.containing.type
+            container = field.enclosingElement.type
         )
     }
 
@@ -136,15 +135,6 @@
         return env.resolver.overrides(this, other)
     }
 
-    override fun copyTo(newContainer: XTypeElement): XMethodElement {
-        check(newContainer is KspTypeElement)
-        return create(
-            env = env,
-            field = field.copyTo(newContainer),
-            accessor = accessor
-        )
-    }
-
     private class Getter(
         env: KspProcessingEnv,
         field: KspFieldElement,
@@ -282,9 +272,8 @@
             }
 
             val field = KspFieldElement(
-                env,
-                accessor.receiver,
-                enclosingType
+                env = env,
+                declaration = accessor.receiver,
             )
             return create(
                 env = env,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
index d7b4a67..1c7bf1e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
@@ -26,6 +26,8 @@
 import androidx.room.compiler.processing.ksp.KspMethodElement
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspType
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeReference
 
 internal class KspSyntheticReceiverParameterElement(
@@ -52,16 +54,17 @@
     override val hasDefaultValue: Boolean
         get() = false
 
-    private val jvmTypeResolutionScope by lazy {
-        KspJvmTypeResolutionScope.MethodParameter(
+    private fun jvmTypeResolutionScope(container: KSDeclaration?): KspJvmTypeResolutionScope {
+        return KspJvmTypeResolutionScope.MethodParameter(
             kspExecutableElement = enclosingElement,
             parameterIndex = 0, // Receiver param is the 1st one
-            annotated = enclosingElement.declaration
+            annotated = enclosingElement.declaration,
+            container = container
         )
     }
 
-    override val type: XType by lazy {
-        env.wrap(receiverType).withJvmTypeResolver(jvmTypeResolutionScope)
+    override val type: KspType by lazy {
+        asMemberOf(enclosingElement.enclosingElement.type?.ksType)
     }
 
     override val fallbackLocationText: String
@@ -75,18 +78,29 @@
     }
 
     override fun asMemberOf(other: XType): KspType {
+        if (closestMemberContainer.type?.isSameType(other) != false) {
+            return type
+        }
         check(other is KspType)
+        return asMemberOf(other.ksType)
+    }
+
+    private fun asMemberOf(ksType: KSType?): KspType {
         val asMemberReceiverType = receiverType.resolve().let {
-            if (it.isError) {
+            if (ksType == null || it.isError) {
                 return@let it
             }
-            val asMember = enclosingElement.declaration.asMemberOf(other.ksType)
+            val asMember = enclosingElement.declaration.asMemberOf(ksType)
             checkNotNull(asMember.extensionReceiverType)
         }
         return env.wrap(
             originatingReference = receiverType,
             ksType = asMemberReceiverType,
-        ).withJvmTypeResolver(jvmTypeResolutionScope)
+        ).withJvmTypeResolver(
+            jvmTypeResolutionScope(
+                container = ksType?.declaration
+            )
+        )
     }
 
     override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
index 4054f04..0dcb133 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
@@ -72,7 +72,7 @@
                                 declaration = method
                             )
                             assertWithMessage(pkg).that(
-                                element.containing.isTypeElement()
+                                element.enclosingElement.isTypeElement()
                             ).isFalse()
                             assertWithMessage(pkg).that(element.isStatic()).isTrue()
                         }
@@ -86,7 +86,7 @@
                                 declaration = it
                             )
                             assertWithMessage(pkg).that(
-                                element.containing.isTypeElement()
+                                element.enclosingElement.isTypeElement()
                             ).isFalse()
                             assertWithMessage(pkg).that(element.isStatic()).isTrue()
                         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
index 9599c6c..7843716 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
@@ -62,7 +62,7 @@
 
     private fun XTestInvocation.assertFieldType(fieldName: String, expectedTypeName: String) {
         val sub = processingEnv.requireTypeElement("SubClass")
-        val subField = sub.getField(fieldName).type.typeName.toString()
+        val subField = sub.getField(fieldName).asMemberOf(sub.type).typeName.toString()
         assertThat(subField).isEqualTo(expectedTypeName)
 
         val base = processingEnv.requireTypeElement("BaseClass")
@@ -77,19 +77,19 @@
     ) {
         val sub = processingEnv.requireTypeElement("SubClass")
         val subMethod = sub.getMethodByJvmName(methodName)
-        val subParam = subMethod.getParameter(paramName)
-        assertThat(subParam.type.typeName.toString()).isEqualTo(expectedTypeName)
+        val paramIndex = subMethod.parameters.indexOf(subMethod.getParameter(paramName))
+        val subMethodParam = subMethod.asMemberOf(sub.type).parameterTypes[paramIndex]
+        assertThat(subMethodParam.typeName.toString()).isEqualTo(expectedTypeName)
 
         val base = processingEnv.requireTypeElement("BaseClass")
-        val baseMethod = base.getMethodByJvmName(methodName).asMemberOf(sub.type)
-        val paramIndex = subMethod.parameters.indexOf(subParam)
-        assertThat(baseMethod.parameterTypes[paramIndex].typeName.toString())
-            .isEqualTo(expectedTypeName)
+        val baseMethod = base.getMethodByJvmName(methodName)
+        val baseMethodParam = baseMethod.asMemberOf(sub.type).parameterTypes[paramIndex]
+        assertThat(baseMethodParam.typeName.toString()).isEqualTo(expectedTypeName)
     }
 
     private fun XTestInvocation.assertReturnType(methodName: String, expectedTypeName: String) {
         val sub = processingEnv.requireTypeElement("SubClass")
-        val subMethod = sub.getMethodByJvmName(methodName)
+        val subMethod = sub.getMethodByJvmName(methodName).asMemberOf(sub.type)
         assertThat(subMethod.returnType.typeName.toString()).isEqualTo(expectedTypeName)
 
         val base = processingEnv.requireTypeElement("BaseClass")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index a607bcc..1f1a834 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -189,21 +189,21 @@
         runProcessorTest(
             listOf(genericBase, boundedChild)
         ) {
-            fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
+            fun validateMethodElement(
+                element: XTypeElement,
+                tTypeName: TypeName,
+                rTypeName: TypeName
+            ) {
                 element.getMethodByJvmName("returnT").let { method ->
                     assertThat(method.parameters).isEmpty()
                     assertThat(method.returnType.typeName).isEqualTo(tTypeName)
                 }
                 element.getMethodByJvmName("receiveT").let { method ->
-                    method.getParameter("param1").let { param ->
-                        assertThat(param.type.typeName).isEqualTo(tTypeName)
-                    }
+                    assertThat(method.getParameter("param1").type.typeName).isEqualTo(tTypeName)
                     assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
                 }
                 element.getMethodByJvmName("receiveR").let { method ->
-                    method.getParameter("param1").let { param ->
-                        assertThat(param.type.typeName).isEqualTo(rTypeName)
-                    }
+                    assertThat(method.getParameter("param1").type.typeName).isEqualTo(rTypeName)
                     assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
                 }
                 element.getMethodByJvmName("returnR").let { method ->
@@ -211,12 +211,48 @@
                     assertThat(method.returnType.typeName).isEqualTo(rTypeName)
                 }
             }
-            validateElement(
+            fun validateMethodTypeAsMemberOf(
+                element: XTypeElement,
+                tTypeName: TypeName,
+                rTypeName: TypeName
+            ) {
+                element.getMethodByJvmName("returnT").asMemberOf(element.type).let { method ->
+                    assertThat(method.parameterTypes).isEmpty()
+                    assertThat(method.returnType.typeName).isEqualTo(tTypeName)
+                }
+                element.getMethodByJvmName("receiveT").asMemberOf(element.type).let { method ->
+                    assertThat(method.parameterTypes).hasSize(1)
+                    assertThat(method.parameterTypes[0].typeName).isEqualTo(tTypeName)
+                    assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
+                }
+                element.getMethodByJvmName("receiveR").asMemberOf(element.type).let { method ->
+                    assertThat(method.parameterTypes).hasSize(1)
+                    assertThat(method.parameterTypes[0].typeName).isEqualTo(rTypeName)
+                    assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
+                }
+                element.getMethodByJvmName("returnR").let { method ->
+                    assertThat(method.parameters).isEmpty()
+                    assertThat(method.returnType.typeName).isEqualTo(rTypeName)
+                }
+            }
+
+            validateMethodElement(
                 element = it.processingEnv.requireTypeElement("foo.bar.Base"),
                 tTypeName = TypeVariableName.get("T"),
                 rTypeName = TypeVariableName.get("R")
             )
-            validateElement(
+            validateMethodElement(
+                element = it.processingEnv.requireTypeElement("foo.bar.Child"),
+                tTypeName = TypeVariableName.get("T"),
+                rTypeName = TypeVariableName.get("R")
+            )
+
+            validateMethodTypeAsMemberOf(
+                element = it.processingEnv.requireTypeElement("foo.bar.Base"),
+                tTypeName = TypeVariableName.get("T"),
+                rTypeName = TypeVariableName.get("R")
+            )
+            validateMethodTypeAsMemberOf(
                 element = it.processingEnv.requireTypeElement("foo.bar.Child"),
                 tTypeName = String::class.className(),
                 rTypeName = TypeVariableName.get("R")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index fda9147..9233b1a 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -733,16 +733,6 @@
     }
 
     @Test
-    fun genericToPrimitiveOverrides_methodElement() {
-        genericToPrimitiveOverrides(asMemberOf = false)
-    }
-
-    @Test
-    fun genericToPrimitiveOverrides_asMemberOf() {
-        genericToPrimitiveOverrides(asMemberOf = true)
-    }
-
-    @Test
     fun defaultMethodParameters() {
         fun buildSource(pkg: String) = Source.kotlin(
             "Foo.kt",
@@ -1047,7 +1037,8 @@
     }
 
     // see b/160258066
-    private fun genericToPrimitiveOverrides(asMemberOf: Boolean) {
+    @Test
+    public fun genericToPrimitiveOverrides() {
         val source = Source.kotlin(
             "Foo.kt",
             """
@@ -1090,21 +1081,14 @@
                         buildString {
                             append(methodElement.jvmName)
                             append("(")
-                            val paramTypes = if (asMemberOf) {
-                                methodElement.asMemberOf(this@methodsSignature.type).parameterTypes
-                            } else {
-                                methodElement.parameters.map { it.type }
-                            }
+                            val enclosingType = this@methodsSignature.type
+                            val paramTypes = methodElement.asMemberOf(enclosingType).parameterTypes
                             val paramsSignature = paramTypes.joinToString(",") {
                                 it.typeName.toString()
                             }
                             append(paramsSignature)
                             append("):")
-                            val returnType = if (asMemberOf) {
-                                methodElement.asMemberOf(this@methodsSignature.type).returnType
-                            } else {
-                                methodElement.returnType
-                            }
+                            val returnType = methodElement.asMemberOf(enclosingType).returnType
                             append(returnType.typeName)
                         }
                     }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index 93eff55..324eeb6 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -102,7 +102,7 @@
                 assertThat(subjects).isNotEmpty()
                 subjects.forEach {
                     callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
-                    callback(it.getMethodByJvmName(methodName).executableType)
+                    callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
                 }
             }
 
@@ -169,7 +169,7 @@
                 assertThat(subjects).isNotEmpty()
                 subjects.forEach {
                     callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
-                    callback(it.getMethodByJvmName(methodName).executableType)
+                    callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
                 }
             }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 0c1d116..c1134d4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -395,7 +395,7 @@
 
             subClass.getField("genericProp").let { field ->
                 // this is tricky because even though it is non-null it, it should still be boxed
-                assertThat(field.type.typeName).isEqualTo(TypeName.INT.box())
+                assertThat(field.asMemberOf(subClass.type).typeName).isEqualTo(TypeName.INT.box())
             }
         }
     }
@@ -1561,6 +1561,239 @@
         }
     }
 
+    @Test
+    fun inheritedGenericMethod() {
+        runProcessorTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test.ConcreteClass.kt",
+                    """
+                    package test;
+                    class ConcreteClass: AbstractClass<Foo, Bar>() {}
+                    abstract class AbstractClass<T1, T2> {
+                        fun method(t1: T1, t2: T2): T2 {
+                          return t2
+                        }
+                    }
+                    class Foo
+                    class Bar
+                    """.trimIndent()
+                )
+            )
+        ) { invocation ->
+            val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+            val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+            val concreteClassMethod = concreteClass.getMethodByJvmName("method")
+            val abstractClassMethod = abstractClass.getMethodByJvmName("method")
+            val fooType = invocation.processingEnv.requireType("test.Foo")
+            val barType = invocation.processingEnv.requireType("test.Bar")
+
+            fun checkMethodElement(method: XMethodElement) {
+                checkMethodElement(
+                    method = method,
+                    name = "method",
+                    enclosingElement = abstractClass,
+                    returnType = TypeVariableName.get("T2"),
+                    parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+                )
+                checkMethodType(
+                    methodType = method.executableType,
+                    returnType = TypeVariableName.get("T2"),
+                    parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+                )
+                checkMethodType(
+                    methodType = method.asMemberOf(abstractClass.type),
+                    returnType = TypeVariableName.get("T2"),
+                    parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+                )
+                checkMethodType(
+                    methodType = method.asMemberOf(concreteClass.type),
+                    returnType = barType.typeName,
+                    parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+                )
+            }
+
+            assertThat(concreteClassMethod).isEqualTo(abstractClassMethod)
+            checkMethodElement(concreteClassMethod)
+            checkMethodElement(abstractClassMethod)
+        }
+    }
+
+    @Test
+    fun overriddenGenericMethod() {
+        runProcessorTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test.ConcreteClass.kt",
+                    """
+                    package test;
+                    class ConcreteClass: AbstractClass<Foo, Bar>() {
+                        override fun method(t1: Foo, t2: Bar): Bar {
+                          return t2
+                        }
+                    }
+                    abstract class AbstractClass<T1, T2> {
+                        abstract fun method(t1: T1, t2: T2): T2
+                    }
+                    class Foo
+                    class Bar
+                    """.trimIndent()
+                )
+            )
+        ) { invocation ->
+            val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+            val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+            val concreteClassMethod = concreteClass.getMethodByJvmName("method")
+            val abstractClassMethod = abstractClass.getMethodByJvmName("method")
+            val fooType = invocation.processingEnv.requireType("test.Foo")
+            val barType = invocation.processingEnv.requireType("test.Bar")
+
+            assertThat(concreteClassMethod).isNotEqualTo(abstractClassMethod)
+            assertThat(concreteClassMethod.overrides(abstractClassMethod, concreteClass)).isTrue()
+
+            // Check the abstract method and method type
+            checkMethodElement(
+                method = abstractClassMethod,
+                name = "method",
+                enclosingElement = abstractClass,
+                returnType = TypeVariableName.get("T2"),
+                parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+            )
+            checkMethodType(
+                methodType = abstractClassMethod.executableType,
+                returnType = TypeVariableName.get("T2"),
+                parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+            )
+            checkMethodType(
+                methodType = abstractClassMethod.asMemberOf(abstractClass.type),
+                returnType = TypeVariableName.get("T2"),
+                parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+            )
+            checkMethodType(
+                methodType = abstractClassMethod.asMemberOf(concreteClass.type),
+                returnType = barType.typeName,
+                parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+            )
+
+            // Check the concrete method and method type
+            checkMethodElement(
+                method = concreteClassMethod,
+                name = "method",
+                enclosingElement = concreteClass,
+                returnType = barType.typeName,
+                parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+            )
+            checkMethodType(
+                methodType = concreteClassMethod.executableType,
+                returnType = barType.typeName,
+                parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+            )
+            checkMethodType(
+                methodType = concreteClassMethod.asMemberOf(concreteClass.type),
+                returnType = barType.typeName,
+                parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+            )
+        }
+    }
+
+    private fun checkMethodElement(
+        method: XMethodElement,
+        name: String,
+        enclosingElement: XTypeElement,
+        returnType: TypeName,
+        parameterTypes: Array<TypeName>
+    ) {
+        assertThat(method.name).isEqualTo(name)
+        assertThat(method.enclosingElement).isEqualTo(enclosingElement)
+        assertThat(method.returnType.typeName).isEqualTo(returnType)
+        assertThat(method.parameters.map { it.type.typeName })
+            .containsExactly(*parameterTypes)
+            .inOrder()
+    }
+    private fun checkMethodType(
+        methodType: XMethodType,
+        returnType: TypeName,
+        parameterTypes: Array<TypeName>
+    ) {
+        assertThat(methodType.returnType.typeName).isEqualTo(returnType)
+        assertThat(methodType.parameterTypes.map { it.typeName })
+            .containsExactly(*parameterTypes)
+            .inOrder()
+    }
+
+    @Test
+    fun overriddenGenericConstructor() {
+        runProcessorTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test.ConcreteClass.kt",
+                    """
+                    package test;
+                    class ConcreteClass(foo: Foo): AbstractClass<Foo>(foo) {}
+                    abstract class AbstractClass<T>(t: T)
+                    class Foo
+                    """.trimIndent()
+                )
+            )
+        ) { invocation ->
+            val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+            val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+            val fooType = invocation.processingEnv.requireType("test.Foo")
+
+            fun checkConstructorParameters(
+                typeElement: XTypeElement,
+                expectedParameters: Array<TypeName>
+            ) {
+                assertThat(typeElement.getConstructors()).hasSize(1)
+                val constructor = typeElement.getConstructors()[0]
+                assertThat(constructor.parameters.map { it.type.typeName })
+                    .containsExactly(*expectedParameters)
+                    .inOrder()
+            }
+
+            checkConstructorParameters(abstractClass, arrayOf(TypeVariableName.get("T")))
+            checkConstructorParameters(concreteClass, arrayOf(fooType.typeName))
+        }
+    }
+
+    @Test
+    fun inheritedGenericField() {
+        runProcessorTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test.ConcreteClass.kt",
+                    """
+                    package test;
+                    class ConcreteClass: AbstractClass<Foo>()
+                    abstract class AbstractClass<T> {
+                        val field: T = TODO()
+                    }
+                    class Foo
+                    """.trimIndent()
+                )
+            )
+        ) { invocation ->
+            val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+            val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+            val concreteClassField = concreteClass.getField("field")
+            val abstractClassField = abstractClass.getField("field")
+            val fooType = invocation.processingEnv.requireType("test.Foo")
+
+            fun checkFieldElement(field: XFieldElement) {
+                assertThat(field.name).isEqualTo("field")
+                assertThat(field.type.typeName).isEqualTo(TypeVariableName.get("T"))
+                assertThat(field.asMemberOf(abstractClass.type).typeName)
+                    .isEqualTo(TypeVariableName.get("T"))
+                assertThat(field.asMemberOf(concreteClass.type).typeName)
+                    .isEqualTo(fooType.typeName)
+            }
+
+            assertThat(concreteClassField).isEqualTo(abstractClassField)
+            checkFieldElement(abstractClassField)
+            checkFieldElement(concreteClassField)
+        }
+    }
+
     /**
      * it is good to exclude methods coming from Object when testing as they differ between KSP
      * and KAPT but irrelevant for Room.
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 5115be6..f14cebf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -92,9 +92,10 @@
                     }
                     .forEach { method ->
                         val testKey = method.createNewUniqueKey(klass.qualifiedName)
+                        val methodType = method.asMemberOf(klass.type)
                         val types = listOf(
-                            method.returnType
-                        ) + method.parameters.map { it.type }
+                            methodType.returnType
+                        ) + methodType.parameterTypes
                         output[testKey] = types.map {
                             it.typeName
                         }
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 9a4310c..13d97c6 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -260,19 +260,23 @@
 }
 
 def checkArtifactContentsTask = tasks.register("checkArtifactTask", CheckArtifactTask) {
-    it.artifactInputs.from {
-        ((PublishToMavenRepository) project.tasks
-                .named("publishMavenPublicationToMavenRepository").get()).getPublication()
-                .artifacts.matching {
-            it.classifier == null
-        }.collect {
-            it.file
+    def pomTask = (GenerateMavenPom) project.tasks.named("generatePomFileForMavenPublication").get()
+    it.pomFile = pomTask.destination
+}
+
+afterEvaluate {
+    def publishTaskProvider = project.tasks.named("publishMavenPublicationToMavenRepository")
+    checkArtifactContentsTask.configure {
+        it.artifactInputs.from {
+            publishTaskProvider.map {
+                ((PublishToMavenRepository) it).getPublication().artifacts.matching {
+                    it.classifier == null
+                }.collect {
+                    it.file
+                }
+            }
         }
     }
-    def pomTask = (GenerateMavenPom) project.tasks
-            .named("generatePomFileForMavenPublication").get()
-    it.pomFile = pomTask.destination
-    it.dependsOn("publishMavenPublicationToMavenRepository")
 }
 
 // make sure we validate published artifacts on the build server.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
index bd01c36..104295d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
@@ -268,14 +268,24 @@
             if (it.jvmName != unannotated.jvmName) {
                 return@firstOrNull false
             }
-            if (!it.returnType.boxed().isSameType(unannotated.returnType.boxed())) {
-                return@firstOrNull false
-            }
             if (it.parameters.size != unannotated.parameters.size) {
                 return@firstOrNull false
             }
+
+            // Get unannotated as a member of annotated's enclosing type before comparing
+            // in case unannotated contains type parameters that need to be resolved.
+            val annotatedEnclosingType = it.enclosingElement.type
+            val unannotatedType = if (annotatedEnclosingType == null) {
+                unannotated.executableType
+            } else {
+                unannotated.asMemberOf(annotatedEnclosingType)
+            }
+
+            if (!it.returnType.boxed().isSameType(unannotatedType.returnType.boxed())) {
+                return@firstOrNull false
+            }
             for (i in it.parameters.indices) {
-                if (it.parameters[i].type.boxed() != unannotated.parameters[i].type.boxed()) {
+                if (it.parameters[i].type.boxed() != unannotatedType.parameterTypes[i].boxed()) {
                     return@firstOrNull false
                 }
             }
diff --git a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
index b4768fc..bc82786 100644
--- a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
+++ b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
@@ -20,6 +20,7 @@
 
 import android.database.AbstractWindowedCursor
 import android.database.Cursor
+import android.database.sqlite.SQLiteConstraintException
 import android.os.Build
 import android.os.CancellationSignal
 import androidx.annotation.RestrictTo
@@ -30,7 +31,6 @@
 import java.io.File
 import java.io.FileInputStream
 import java.io.IOException
-import java.lang.IllegalStateException
 import java.nio.ByteBuffer
 
 /**
@@ -121,7 +121,7 @@
     db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor ->
         if (cursor.count > 0) {
             val errorMsg = processForeignKeyCheckFailure(cursor)
-            throw IllegalStateException(errorMsg)
+            throw SQLiteConstraintException(errorMsg)
         }
     }
 }
diff --git a/settings.gradle b/settings.gradle
index d3d7d62..12050a6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -844,8 +844,8 @@
 includeProject(":tracing:tracing-perfetto-common")
 includeProject(":transition:transition", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":transition:transition-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":tv:tv-foundation", [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":tv:tv-material", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":tv:tv-foundation", [BuildType.COMPOSE])
+includeProject(":tv:tv-material", [BuildType.COMPOSE])
 includeProject(":tvprovider:tvprovider", [BuildType.MAIN])
 includeProject(":vectordrawable:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":vectordrawable:vectordrawable", [BuildType.MAIN])
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index 0e44a89..07c45f1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -364,7 +364,7 @@
             return new SinglyLinkedList<T>(new Node<T>(data, rest.mHead));
         }
 
-        @SuppressWarnings("MissingOverride")
+        @Override
         public Iterator<T> iterator() {
             return new Iterator<T>() {
                 private Node<T> mNext = mHead;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
index 4467ef9..7e68ac9 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
@@ -19,21 +19,23 @@
 import androidx.annotation.NonNull;
 
 /** An enumeration used to specify the primary direction of certain gestures. */
-@SuppressWarnings("ImmutableEnumChecker")
 public enum Direction {
     LEFT, RIGHT, UP, DOWN;
 
-    private Direction mOpposite;
-    static {
-        LEFT.mOpposite = RIGHT;
-        RIGHT.mOpposite = LEFT;
-        UP.mOpposite = DOWN;
-        DOWN.mOpposite = UP;
-    }
-
     /** Returns the reverse of the given direction. */
     @NonNull
     public static Direction reverse(@NonNull Direction direction) {
-        return direction.mOpposite;
+        switch (direction) {
+            case LEFT:
+                return RIGHT;
+            case RIGHT:
+                return LEFT;
+            case UP:
+                return DOWN;
+            case DOWN:
+                return UP;
+            default:
+                throw new AssertionError(direction);
+        }
     }
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
index 89cabe7..10c14c5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
@@ -24,8 +24,6 @@
  */
 @SuppressWarnings("TypeNameShadowing")
 public abstract class EventCondition<R> extends Condition<AccessibilityEvent, Boolean> {
-    @SuppressWarnings({"HiddenAbstractMethod", "MissingOverride"})
-    abstract Boolean apply(AccessibilityEvent event);
 
     @SuppressWarnings("HiddenAbstractMethod")
     abstract R getResult();
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 4bc07bf..8464613 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -61,25 +61,11 @@
 
     /** Comparator for sorting PointerGestures by start times. */
     private static final Comparator<PointerGesture> START_TIME_COMPARATOR =
-            new Comparator<PointerGesture>() {
-
-        @Override
-        @SuppressWarnings("BadComparable")
-        public int compare(PointerGesture o1, PointerGesture o2) {
-            return (int)(o1.delay() - o2.delay());
-        }
-    };
+            (o1, o2) -> Long.compare(o1.delay(), o2.delay());
 
     /** Comparator for sorting PointerGestures by end times. */
     private static final Comparator<PointerGesture> END_TIME_COMPARATOR =
-            new Comparator<PointerGesture>() {
-
-        @Override
-        @SuppressWarnings("BadComparable")
-        public int compare(PointerGesture o1, PointerGesture o2) {
-            return (int)((o1.delay() + o2.duration()) - (o2.delay() + o2.duration()));
-        }
-    };
+            (o1, o2) -> Long.compare(o1.delay() + o1.duration(), o2.delay() + o2.duration());
 
 
     // Private constructor.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 2997a86..3177ad2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -75,8 +75,7 @@
     /**
      * Predicate for waiting for any of the events specified in the mask
      */
-    @SuppressWarnings("ClassCanBeStatic")
-    class WaitForAnyEventPredicate implements AccessibilityEventFilter {
+    static class WaitForAnyEventPredicate implements AccessibilityEventFilter {
         int mMask;
         WaitForAnyEventPredicate(int mask) {
             mMask = mask;
@@ -98,8 +97,7 @@
      * a ctor passed list with matching events. User of this predicate must recycle
      * all populated events in the events list.
      */
-    @SuppressWarnings("ClassCanBeStatic")
-    class EventCollectingPredicate implements AccessibilityEventFilter {
+    static class EventCollectingPredicate implements AccessibilityEventFilter {
         int mMask;
         List<AccessibilityEvent> mEventsList;
 
@@ -125,8 +123,7 @@
     /**
      * Predicate for waiting for every event specified in the mask to be matched at least once
      */
-    @SuppressWarnings("ClassCanBeStatic")
-    class WaitForAllEventPredicate implements AccessibilityEventFilter {
+    static class WaitForAllEventPredicate implements AccessibilityEventFilter {
         int mMask;
         WaitForAllEventPredicate(int mask) {
             mMask = mask;
@@ -474,12 +471,10 @@
      * @param drag when true, the swipe becomes a drag swipe
      * @return true if the swipe executed successfully
      */
-    @SuppressWarnings("UnusedVariable")
     public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
-        boolean ret = false;
+        boolean ret;
         int swipeSteps = steps;
-        double xStep = 0;
-        double yStep = 0;
+        double xStep, yStep;
 
         // avoid a divide by zero
         if(swipeSteps == 0)
@@ -515,12 +510,10 @@
      * @param segmentSteps steps to inject between two Points
      * @return true on success
      */
-    @SuppressWarnings("UnusedVariable")
     public boolean swipe(Point[] segments, int segmentSteps) {
-        boolean ret = false;
+        boolean ret;
         int swipeSteps = segmentSteps;
-        double xStep = 0;
-        double yStep = 0;
+        double xStep, yStep;
 
         // avoid a divide by zero
         if(segmentSteps == 0)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
index c2dadc8..40883a1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
@@ -57,21 +57,19 @@
     }
 
     /** Adds an action which pauses for the specified amount of {@code time} in milliseconds. */
-    @SuppressWarnings("UnnecessaryParentheses")
     public PointerGesture pause(long time) {
         if (time < 0) {
             throw new IllegalArgumentException("time cannot be negative");
         }
         mActions.addLast(new PointerPauseAction(mActions.peekLast().end, time));
-        mDuration += (mActions.peekLast().duration);
+        mDuration += mActions.peekLast().duration;
         return this;
     }
 
     /** Adds an action that moves the pointer to {@code dest} at {@code speed} pixels per second. */
-    @SuppressWarnings("UnnecessaryParentheses")
     public PointerGesture move(Point dest, int speed) {
         mActions.addLast(new PointerLinearMoveAction(mActions.peekLast().end, dest, speed));
-        mDuration += (mActions.peekLast().duration);
+        mDuration += mActions.peekLast().duration;
         return this;
     }
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index d6b00fa..ab8466c1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -136,7 +136,7 @@
     }
 
     /** Returns whether there is a match for the given {@code selector} criteria. */
-    @SuppressWarnings("MissingOverride")
+    @Override
     public boolean hasObject(BySelector selector) {
         AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
         if (node != null) {
@@ -150,14 +150,14 @@
      * Returns the first object to match the {@code selector} criteria,
      * or null if no matching objects are found.
      */
-    @SuppressWarnings("MissingOverride")
+    @Override
     public UiObject2 findObject(BySelector selector) {
         AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
         return node != null ? new UiObject2(this, selector, node) : null;
     }
 
     /** Returns all objects that match the {@code selector} criteria. */
-    @SuppressWarnings("MissingOverride")
+    @Override
     public List<UiObject2> findObjects(BySelector selector) {
         List<UiObject2> ret = new ArrayList<UiObject2>();
         for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
@@ -964,7 +964,6 @@
      *         window does not have the specified package name
      * @since API Level 16
      */
-    @SuppressWarnings("UndefinedEquals")
     public boolean waitForWindowUpdate(final String packageName, long timeout) {
         Tracer.trace(packageName, timeout);
         if (packageName != null) {
@@ -981,7 +980,7 @@
             @Override
             public boolean accept(AccessibilityEvent t) {
                 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
-                    return packageName == null || packageName.equals(t.getPackageName());
+                    return packageName == null || packageName.contentEquals(t.getPackageName());
                 }
                 return false;
             }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index f1202f9..86ca021 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -1080,7 +1080,6 @@
      *         <code>false</code> otherwise
      * @since API Level 18
      */
-    @SuppressWarnings("NarrowingCompoundAssignment")
     public boolean performTwoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1,
             Point endPoint2, int steps) {
 
@@ -1119,10 +1118,10 @@
             p2.size = 1;
             points2[i] = p2;
 
-            eventX1 += stepX1;
-            eventY1 += stepY1;
-            eventX2 += stepX2;
-            eventY2 += stepY2;
+            eventX1 = (int) (eventX1 + stepX1);
+            eventY1 = (int) (eventY1 + stepY1);
+            eventX2 = (int) (eventX2 + stepX2);
+            eventY2 = (int) (eventY2 + stepY2);
         }
 
         // ending pointers coordinates
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 2e1618a..a4332c6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -99,19 +99,15 @@
 
     /** {@inheritDoc} */
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object object) {
         if (this == object) {
             return true;
         }
-        if (object == null) {
-            return false;
-        }
-        if (getClass() != object.getClass()) {
+        if (!(object instanceof UiObject2)) {
             return false;
         }
         try {
-            UiObject2 other = (UiObject2)object;
+            UiObject2 other = (UiObject2) object;
             return getAccessibilityNodeInfo().equals(other.getAccessibilityNodeInfo());
         } catch (StaleObjectException e) {
             return false;
@@ -209,7 +205,7 @@
      * Searches all elements under this object and returns the first object to match the criteria,
      * or null if no matching objects are found.
      */
-    @SuppressWarnings("MissingOverride")
+    @Override
     public UiObject2 findObject(BySelector selector) {
         AccessibilityNodeInfo node =
                 ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());
@@ -217,7 +213,7 @@
     }
 
     /** Searches all elements under this object and returns all objects that match the criteria. */
-    @SuppressWarnings("MissingOverride")
+    @Override
     public List<UiObject2> findObjects(BySelector selector) {
         List<UiObject2> ret = new ArrayList<UiObject2>();
         for (AccessibilityNodeInfo node :
@@ -695,7 +691,6 @@
      * Set the text content by sending individual key codes.
      * @hide
      */
-    @SuppressWarnings("UndefinedEquals")
     public void legacySetText(String text) {
         AccessibilityNodeInfo node = getAccessibilityNodeInfo();
 
@@ -705,7 +700,7 @@
         }
 
         CharSequence currentText = node.getText();
-        if (!text.equals(currentText)) {
+        if (!text.contentEquals(currentText)) {
             InteractionController ic = getDevice().getInteractionController();
 
             // Long click left + center
@@ -725,7 +720,6 @@
     }
 
     /** Sets the text content if this object is an editable field. */
-    @SuppressWarnings("UndefinedEquals")
     public void setText(String text) {
         AccessibilityNodeInfo node = getAccessibilityNodeInfo();
 
@@ -745,7 +739,7 @@
             }
         } else {
             CharSequence currentText = node.getText();
-            if (!text.equals(currentText)) {
+            if (!text.contentEquals(currentText)) {
                 // Give focus to the object. Expect this to fail if the object already has focus.
                 if (!node.performAction(AccessibilityNodeInfo.ACTION_FOCUS) && !node.isFocused()) {
                     // TODO: Decide if we should throw here
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index e6f50d0..567cf50 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -1 +1,279 @@
 // Signature format: 4.0
+package androidx.tv.foundation {
+
+  public final class MarioScrollableKt {
+    method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+  }
+
+  public final class PivotOffsets {
+    ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+    method public float getChildFraction();
+    method public float getParentFraction();
+    property public final float childFraction;
+    property public final float parentFraction;
+  }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+  public final class LazyBeyondBoundsModifierKt {
+  }
+
+  public final class LazyListPinningModifierKt {
+  }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+  public final class LazyGridDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyGridItemPlacementAnimatorKt {
+  }
+
+  public final class LazyGridItemsProviderImplKt {
+  }
+
+  public final class LazyGridKt {
+  }
+
+  public final class LazyGridMeasureKt {
+  }
+
+  public final class LazyGridScrollingKt {
+  }
+
+  public final class LazyGridSpanKt {
+    method public static long TvGridItemSpan(int currentLineSpan);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  @androidx.compose.runtime.Stable public interface TvGridCells {
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Adaptive(float minSize);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Fixed(int count);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+    method public int getCurrentLineSpan();
+    property public final int currentLineSpan;
+  }
+
+  public sealed interface TvLazyGridItemInfo {
+    method public int getColumn();
+    method public int getIndex();
+    method public Object getKey();
+    method public long getOffset();
+    method public int getRow();
+    method public long getSize();
+    property public abstract int column;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract long offset;
+    property public abstract int row;
+    property public abstract long size;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  public static final class TvLazyGridItemInfo.Companion {
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+    method public int getMaxCurrentLineSpan();
+    method public int getMaxLineSpan();
+    property public abstract int maxCurrentLineSpan;
+    property public abstract int maxLineSpan;
+  }
+
+  public sealed interface TvLazyGridLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+    method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+  }
+
+  public static final class TvLazyGridState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+  }
+
+  public final class TvLazyGridStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+  public final class LazyDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyListHeadersKt {
+  }
+
+  public final class LazyListItemPlacementAnimatorKt {
+  }
+
+  public final class LazyListItemsProviderImplKt {
+  }
+
+  public final class LazyListKt {
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
+  public final class LazyListScrollingKt {
+  }
+
+  public final class LazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  public interface TvLazyListItemInfo {
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+  }
+
+  public sealed interface TvLazyListLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+    method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+  }
+
+  public static final class TvLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+  }
+
+}
+
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index e6f50d0..f6f513b 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -1 +1,281 @@
 // Signature format: 4.0
+package androidx.tv.foundation {
+
+  public final class MarioScrollableKt {
+    method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+  }
+
+  public final class PivotOffsets {
+    ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+    method public float getChildFraction();
+    method public float getParentFraction();
+    property public final float childFraction;
+    property public final float parentFraction;
+  }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+  public final class LazyBeyondBoundsModifierKt {
+  }
+
+  public final class LazyListPinningModifierKt {
+  }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+  public final class LazyGridDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyGridItemPlacementAnimatorKt {
+  }
+
+  public final class LazyGridItemsProviderImplKt {
+  }
+
+  public final class LazyGridKt {
+  }
+
+  public final class LazyGridMeasureKt {
+  }
+
+  public final class LazyGridScrollingKt {
+  }
+
+  public final class LazyGridSpanKt {
+    method public static long TvGridItemSpan(int currentLineSpan);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  @androidx.compose.runtime.Stable public interface TvGridCells {
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Adaptive(float minSize);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Fixed(int count);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+    method public int getCurrentLineSpan();
+    property public final int currentLineSpan;
+  }
+
+  public sealed interface TvLazyGridItemInfo {
+    method public int getColumn();
+    method public int getIndex();
+    method public Object getKey();
+    method public long getOffset();
+    method public int getRow();
+    method public long getSize();
+    property public abstract int column;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract long offset;
+    property public abstract int row;
+    property public abstract long size;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  public static final class TvLazyGridItemInfo.Companion {
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+    method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+    method public int getMaxCurrentLineSpan();
+    method public int getMaxLineSpan();
+    property public abstract int maxCurrentLineSpan;
+    property public abstract int maxLineSpan;
+  }
+
+  public sealed interface TvLazyGridLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+    method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+  }
+
+  public static final class TvLazyGridState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+  }
+
+  public final class TvLazyGridStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+  public final class LazyDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyListHeadersKt {
+  }
+
+  public final class LazyListItemPlacementAnimatorKt {
+  }
+
+  public final class LazyListItemsProviderImplKt {
+  }
+
+  public final class LazyListKt {
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
+  public final class LazyListScrollingKt {
+  }
+
+  public final class LazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  public interface TvLazyListItemInfo {
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+    method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+  }
+
+  public sealed interface TvLazyListLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+    method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+  }
+
+  public static final class TvLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+  }
+
+}
+
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index e6f50d0..567cf50 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -1 +1,279 @@
 // Signature format: 4.0
+package androidx.tv.foundation {
+
+  public final class MarioScrollableKt {
+    method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+  }
+
+  public final class PivotOffsets {
+    ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+    method public float getChildFraction();
+    method public float getParentFraction();
+    property public final float childFraction;
+    property public final float parentFraction;
+  }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+  public final class LazyBeyondBoundsModifierKt {
+  }
+
+  public final class LazyListPinningModifierKt {
+  }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+  public final class LazyGridDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyGridItemPlacementAnimatorKt {
+  }
+
+  public final class LazyGridItemsProviderImplKt {
+  }
+
+  public final class LazyGridKt {
+  }
+
+  public final class LazyGridMeasureKt {
+  }
+
+  public final class LazyGridScrollingKt {
+  }
+
+  public final class LazyGridSpanKt {
+    method public static long TvGridItemSpan(int currentLineSpan);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  @androidx.compose.runtime.Stable public interface TvGridCells {
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Adaptive(float minSize);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+    ctor public TvGridCells.Fixed(int count);
+    method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+    method public int getCurrentLineSpan();
+    property public final int currentLineSpan;
+  }
+
+  public sealed interface TvLazyGridItemInfo {
+    method public int getColumn();
+    method public int getIndex();
+    method public Object getKey();
+    method public long getOffset();
+    method public int getRow();
+    method public long getSize();
+    property public abstract int column;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract long offset;
+    property public abstract int row;
+    property public abstract long size;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  public static final class TvLazyGridItemInfo.Companion {
+    field public static final int UnknownColumn = -1; // 0xffffffff
+    field public static final int UnknownRow = -1; // 0xffffffff
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+    method public int getMaxCurrentLineSpan();
+    method public int getMaxLineSpan();
+    property public abstract int maxCurrentLineSpan;
+    property public abstract int maxLineSpan;
+  }
+
+  public sealed interface TvLazyGridLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+    method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+  }
+
+  public static final class TvLazyGridState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+  }
+
+  public final class TvLazyGridStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+  public final class LazyDslKt {
+    method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class LazyListHeadersKt {
+  }
+
+  public final class LazyListItemPlacementAnimatorKt {
+  }
+
+  public final class LazyListItemsProviderImplKt {
+  }
+
+  public final class LazyListKt {
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
+  public final class LazyListScrollingKt {
+  }
+
+  public final class LazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+  }
+
+  public final class LazySemanticsKt {
+  }
+
+  public interface TvLazyListItemInfo {
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+  }
+
+  public sealed interface TvLazyListLayoutInfo {
+    method public int getAfterContentPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterContentPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+    method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getFirstVisibleItemIndex();
+    method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+    method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int firstVisibleItemIndex;
+    property public final int firstVisibleItemScrollOffset;
+    property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+    property public boolean isScrollInProgress;
+    property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+    field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+  }
+
+  public static final class TvLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+  }
+
+}
+
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index c4f9fe0..4bd3f05 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -14,21 +14,57 @@
  * limitations under the License.
  */
 
+import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import java.security.MessageDigest
+import java.util.stream.Collectors
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
     id("com.android.library")
     id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+
+    api("androidx.annotation:annotation:1.1.0")
+    api(project(":compose:animation:animation"))
+    api(project(':compose:runtime:runtime'))
+    api(project(":compose:ui:ui"))
+
+    implementation(libs.kotlinStdlibCommon)
+    implementation(project(":compose:foundation:foundation"))
+    implementation(project(":compose:foundation:foundation-layout"))
+    implementation(project(":compose:ui:ui-graphics"))
+    implementation(project(":compose:ui:ui-text"))
+    implementation(project(":compose:ui:ui-util"))
+    implementation("androidx.profileinstaller:profileinstaller:1.2.0-alpha02")
+
+    testImplementation(libs.testRules)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.junit)
+    implementation(libs.truth)
+
+    androidTestImplementation(project(":compose:ui:ui-test"))
+    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:test-utils"))
+    androidTestImplementation(libs.testRunner)
 }
 
 android {
     namespace "androidx.tv.foundation"
+    defaultConfig {
+        minSdkVersion 28
+    }
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+    lintOptions {
+        disable 'IllegalExperimentalApiUsage' // TODO (b/233188423): Address before moving to beta
+    }
 }
 
 androidx {
@@ -40,4 +76,477 @@
             "to write Jetpack Compose applications for TV devices by providing " +
             "functionality to support TV specific devices sizes, shapes and d-pad navigation " +
             "supported components. It builds upon the Jetpack Compose libraries."
+    targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=all",
+        ]
+    }
+}
+
+// Functions and tasks to monitor changes in copied files.
+
+task generateMd5 {
+    ext.genMd5 = { fileNameToHash ->
+        MessageDigest digest = MessageDigest.getInstance("MD5")
+        file(fileNameToHash).withInputStream(){is->
+            byte[] buffer = new byte[8192]
+            int read = 0
+            while( (read = is.read(buffer)) > 0) {
+                digest.update(buffer, 0, read);
+            }
+        }
+        byte[] md5sum = digest.digest()
+        BigInteger bigInt = new BigInteger(1, md5sum)
+        bigInt.toString(16).padLeft(32, '0')
+    }
+
+    doLast {
+        String hashValue = genMd5(file)
+        print "value="
+        println hashValue
+    }
+}
+
+List<CopiedClass> copiedClasses = new ArrayList<>();
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/MarioScrollable.kt",
+                "afaf0f2be6b57df076db42d9218f83d9"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/DataIndex.kt",
+                "2aa3c6d2dd05057478e723b2247517e1"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyItemScopeImpl.kt",
+                "31e6796d0d03cb84483396a39fc5b7e7"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
+                "4d71c69f9cb38f741da9cfc4109567dd"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
+                "a74bfa05e68e2b6c2e108f022dfbfa26"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemProviderImpl.kt",
+                "57ff505cbdfa854e15b4fbd9d4a574eb"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemsProvider.kt",
+                "42a2c446c81fba89fd7b8480d063b308"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyList.kt",
+                "c605794683c01c516674436c9ebc1f44"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
+                "95c14abd0367f0f39218c9bdd175b242"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
+                "d4407572c6550d184133f8b3fd37869f"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScopeImpl.kt",
+                "1888e8b115c73b5ea7f33d48d9887845"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrolling.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrolling.kt",
+                "a32b856a1e8740a6a521df04c9d51ed1"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
+                "82df7d370ba5b20309e5191a0af431a0"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListState.kt",
+                "45821a5bf14d3e6e25fee63e61930f57"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
+                "78b09b4d78ec9d761274b9ca8d24f4f7"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
+                "bec4211cb3d91bb936e9f0872864244b"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazySemantics.kt",
+                "739205f656bf107604ba7167e3cee7e7"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/ItemIndex.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
+                "1031b8b91a81c684b3c4584bc93d3fb0"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
+                "6a0b2db56ef38fb1ac004e4fc9847db8"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemInfo.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemInfo.kt",
+                "1f3b13ee45de79bc67ace4133e634600"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+                "0bbc162aab675ca2a34350e3044433e7"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+                "b3ff4600791c73028b8661c0e2b49110"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScope.kt",
+                "1a40313cc5e67b5808586c012bbfb058"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt",
+                "48fdfb1dfa5d39c88d4aa96732192421"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
+                "e5e95e6cad43cec2b0c30bf201e3cae9"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
+                "69dbab5e83deab809219d4d7a9ee7fa8"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+                "b421c5e74856a78982efe0d8a79d10cb"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
+                "2b38f5261ad092d9048cfc4f0a841a1a"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasureResult.kt",
+                "1277598d36d8507d7bf0305cc629a11c"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeImpl.kt",
+                "3296c6edcbd56450ba919df105cb36c0"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeMarker.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeMarker.kt",
+                "0b7ff258a80e2413f89d56ab0ef41b46"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt",
+                "15f2f9bb89c1603aa4b7e7d1f8a2de5a"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
+                "9b3d47322ad526fb17a3d9505a80f673"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
+                "cc63cb4f05cc556e8fcf7504ac0ea57c"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+                "894b9f69a27e247bbe609bdac22bb5ed"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridState.kt",
+                "1e37d8a6f159aabe11f488121de59b70"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
+                "09d9b21d33325a94cac738aad58e2422"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+                "3acdfddfd06eb17aac5dbdd326482e35"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
+                "1104f01e8b1f6eced2401b207114f4a4"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+                "b7b731e6e8fdc520064aaef989575bda"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
+                "dab277484b4ec57a5275095b505f79d4"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyDsl.kt",
+                "8462c0a61f14639f39dd6f76c6a2aebc"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinningModifier.kt",
+                "src/commonMain/kotlin/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
+                "e37450505d13ab0fd1833f136ec8aa3c"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyScopeMarker.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt",
+                "f7b72b3c6bad88868153300b9fbdd922"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt",
+                "6254294540cfadf2d6da1bbbce1611e8"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt",
+                "7571daa18ca079fd6de31d37c3022574"
+        )
+)
+
+copiedClasses.add(
+        new CopiedClass(
+                "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt",
+                "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt",
+                "fa1dffc993bdc486e0819c5d8018cda3"
+        )
+)
+
+task doCopiesNeedUpdate {
+    ext.genMd5 = { fileNameToHash ->
+        try {
+            MessageDigest digest = MessageDigest.getInstance("MD5")
+            file(fileNameToHash).withInputStream() { is ->
+                byte[] buffer = new byte[8192]
+                int read
+                while ((read = is.read(buffer)) > 0) {
+                    digest.update(buffer, 0, read);
+                }
+            }
+            byte[] md5sum = digest.digest()
+            BigInteger bigInt = new BigInteger(1, md5sum)
+            bigInt.toString(16).padLeft(32, '0')
+        } catch (Exception e) {
+            throw new GradleException("Failed for file=$fileNameToHash", e)
+        }
+    }
+
+
+    doLast {
+        List<String> failureFiles = new ArrayList<>()
+        copiedClasses.forEach(copiedClass -> {
+            try {
+                String actualMd5 = genMd5(copiedClass.originalFilePath)
+                if (copiedClass.lastKnownGoodHash != actualMd5) {
+                    failureFiles.add(copiedClass.toString()+ ", actual=" + actualMd5)
+                }
+            } catch (Exception e) {
+                throw new GradleException("Failed for file=${copiedClass.originalFilePath}", e)
+            }
+        })
+
+        if (!failureFiles.isEmpty()) {
+            throw new GradleException(
+                    "Files that were copied have been updated at the source. " +
+                            "Please update the copy and then" +
+                            " update the hash in the compose-foundation build.gradle file." +
+                            failureFiles.stream().collect(Collectors.joining("\n", "\n", "")))
+        }
+    }
+}
+
+class CopiedClass {
+    String originalFilePath
+    String copyFilePath
+    String lastKnownGoodHash
+
+    CopiedClass(String originalFilePath, String copyFilePath, String lastKnownGoodHash) {
+        this.originalFilePath = originalFilePath
+        this.copyFilePath = copyFilePath
+        this.lastKnownGoodHash = lastKnownGoodHash
+    }
+
+    @Override
+    String toString() {
+        return "originalFilePath='" + originalFilePath + '\'' +
+                ", copyFilePath='" + copyFilePath + '\'' +
+                ", lastKnownGoodHash='" + lastKnownGoodHash + '\''
+    }
 }
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt
new file mode 100644
index 0000000..19e2105
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy
+
+import androidx.compose.runtime.MonotonicFrameClock
+import java.util.concurrent.atomic.AtomicLong
+
+class AutoTestFrameClock : MonotonicFrameClock {
+    private val time = AtomicLong(0)
+
+    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+        return onFrame(time.getAndAdd(16_000_000))
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
new file mode 100644
index 0000000..5bf00b4
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+
+open class BaseLazyGridTestWithOrientation(private val orientation: Orientation) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val vertical: Boolean
+        get() = orientation == Orientation.Vertical
+
+    @Stable
+    fun Modifier.crossAxisSize(size: Dp) =
+        if (vertical) {
+            this.width(size)
+        } else {
+            this.height(size)
+        }
+
+    @Stable
+    fun Modifier.mainAxisSize(size: Dp) =
+        if (vertical) {
+            this.height(size)
+        } else {
+            this.width(size)
+        }
+
+    @Stable
+    fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
+        if (vertical) {
+            this.size(crossAxis, mainAxis)
+        } else {
+            this.size(mainAxis, crossAxis)
+        }
+
+    fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertHeightIsEqualTo(expectedSize)
+        } else {
+            assertWidthIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertWidthIsEqualTo(expectedSize)
+        } else {
+            assertHeightIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+        val position = if (vertical) {
+            getUnclippedBoundsInRoot().top
+        } else {
+            getUnclippedBoundsInRoot().left
+        }
+        position.assertIsEqualTo(expected, tolerance = 1.dp)
+    }
+
+    fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun PaddingValues(
+        mainAxis: Dp = 0.dp,
+        crossAxis: Dp = 0.dp
+    ) = PaddingValues(
+        beforeContent = mainAxis,
+        afterContent = mainAxis,
+        beforeContentCrossAxis = crossAxis,
+        afterContentCrossAxis = crossAxis
+    )
+
+    fun PaddingValues(
+        beforeContent: Dp = 0.dp,
+        afterContent: Dp = 0.dp,
+        beforeContentCrossAxis: Dp = 0.dp,
+        afterContentCrossAxis: Dp = 0.dp,
+    ) = if (vertical) {
+        PaddingValues(
+            start = beforeContentCrossAxis,
+            top = beforeContent,
+            end = afterContentCrossAxis,
+            bottom = afterContent
+        )
+    } else {
+        PaddingValues(
+            start = beforeContent,
+            top = beforeContentCrossAxis,
+            end = afterContent,
+            bottom = afterContentCrossAxis
+        )
+    }
+
+    fun TvLazyGridState.scrollBy(offset: Dp) {
+        runBlocking(Dispatchers.Main) {
+            animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+        }
+    }
+
+    fun TvLazyGridState.scrollTo(index: Int) {
+        runBlocking(Dispatchers.Main) {
+            scrollToItem(index)
+        }
+    }
+
+    fun ComposeContentTestRule.keyPress(numberOfPresses: Int = 1) {
+        rule.keyPress(
+            if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
+            numberOfPresses
+        )
+    }
+
+    @Composable
+    fun LazyGrid(
+        cells: Int,
+        modifier: Modifier = Modifier,
+        state: TvLazyGridState = rememberLazyGridState(),
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        reverseLayout: Boolean = false,
+        userScrollEnabled: Boolean = true,
+        crossAxisSpacedBy: Dp = 0.dp,
+        mainAxisSpacedBy: Dp = 0.dp,
+        content: TvLazyGridScope.() -> Unit
+    ) = LazyGrid(
+        TvGridCells.Fixed(cells),
+        modifier,
+        state,
+        contentPadding,
+        reverseLayout,
+        userScrollEnabled,
+        crossAxisSpacedBy,
+        mainAxisSpacedBy,
+        content
+    )
+
+    @Composable
+    fun LazyGrid(
+        cells: TvGridCells,
+        modifier: Modifier = Modifier,
+        state: TvLazyGridState = rememberLazyGridState(),
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        reverseLayout: Boolean = false,
+        userScrollEnabled: Boolean = true,
+        crossAxisSpacedBy: Dp = 0.dp,
+        mainAxisSpacedBy: Dp = 0.dp,
+        content: TvLazyGridScope.() -> Unit
+    ) {
+        if (vertical) {
+            val verticalArrangement = when {
+                mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+                !reverseLayout -> Arrangement.Top
+                else -> Arrangement.Bottom
+            }
+            val horizontalArrangement = when {
+                crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+                else -> Arrangement.Start
+            }
+            TvLazyVerticalGrid(
+                columns = cells,
+                modifier = modifier,
+                state = state,
+                contentPadding = contentPadding,
+                reverseLayout = reverseLayout,
+                userScrollEnabled = userScrollEnabled,
+                verticalArrangement = verticalArrangement,
+                horizontalArrangement = horizontalArrangement,
+                pivotOffsets = PivotOffsets(parentFraction = 0f),
+                content = content
+            )
+        } else {
+            val horizontalArrangement = when {
+                mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+                !reverseLayout -> Arrangement.Start
+                else -> Arrangement.End
+            }
+            val verticalArrangement = when {
+                crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+                else -> Arrangement.Top
+            }
+            TvLazyHorizontalGrid(
+                rows = cells,
+                modifier = modifier,
+                state = state,
+                contentPadding = contentPadding,
+                reverseLayout = reverseLayout,
+                userScrollEnabled = userScrollEnabled,
+                horizontalArrangement = horizontalArrangement,
+                verticalArrangement = verticalArrangement,
+                pivotOffsets = PivotOffsets(parentFraction = 0f),
+                content = content
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt
new file mode 100644
index 0000000..0ed6481
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt
@@ -0,0 +1,617 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyArrangementsTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+    private var smallerItemSize: Dp = Dp.Infinity
+    private var containerSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+        with(rule.density) {
+            smallerItemSize = 40.toDp()
+        }
+        containerSize = itemSize * 5
+    }
+
+    // cases when we have not enough items to fill min constraints:
+
+    @Test
+    fun vertical_defaultArrangementIsTop() {
+        rule.setContent {
+            TvLazyVerticalGrid(
+                modifier = Modifier.requiredSize(containerSize),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Top)
+    }
+
+    @Test
+    fun vertical_centerArrangement() {
+        composeVerticalGridWith(Arrangement.Center)
+        assertArrangementForTwoItems(Arrangement.Center)
+    }
+
+    @Test
+    fun vertical_bottomArrangement() {
+        composeVerticalGridWith(Arrangement.Bottom)
+        assertArrangementForTwoItems(Arrangement.Bottom)
+    }
+
+    @Test
+    fun vertical_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeVerticalGridWith(arrangement)
+        assertArrangementForTwoItems(arrangement)
+    }
+
+    @Test
+    fun horizontal_defaultArrangementIsStart() {
+        rule.setContent {
+            TvLazyHorizontalGrid(
+                modifier = Modifier.requiredSize(containerSize),
+                rows = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun horizontal_centerArrangement() {
+        composeHorizontalWith(Arrangement.Center, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun horizontal_endArrangement() {
+        composeHorizontalWith(Arrangement.End, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun horizontal_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeHorizontalWith(arrangement, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun horizontal_rtl_startArrangement() {
+        composeHorizontalWith(Arrangement.Center, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun horizontal_rtl_endArrangement() {
+        composeHorizontalWith(Arrangement.End, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun horizontal_rtl_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeHorizontalWith(arrangement, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+    }
+
+    // wrap content and spacing
+
+    @Test
+    fun vertical_spacing_affects_wrap_content() {
+        rule.setContent {
+            TvLazyVerticalGrid(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.width(itemSize).testTag(ContainerTag),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun horizontal_spacing_affects_wrap_content() {
+        rule.setContent {
+            TvLazyHorizontalGrid(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.height(itemSize).testTag(ContainerTag),
+                rows = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize * 3)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    // spacing added when we have enough items to fill the viewport
+
+    @Test
+    fun vertical_spacing_scrolledToTheTop() {
+        rule.setContent {
+            TvLazyVerticalGrid(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun vertical_spacing_scrolledToTheBottom() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+                columns = TvGridCells.Fixed(1),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    @Test
+    fun horizontal_spacing_scrolledToTheStart() {
+        rule.setContent {
+            TvLazyHorizontalGrid(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f),
+                rows = TvGridCells.Fixed(1)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun horizontal_spacing_scrolledToTheEnd() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyHorizontalGrid(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+                rows = TvGridCells.Fixed(1),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    @Test
+    fun vertical_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                modifier = Modifier.size(itemSize * 3),
+                state = rememberLazyGridState().also { state = it },
+                verticalArrangement = Arrangement.spacedBy(spacingSize),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun vertical_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                modifier = Modifier.size(itemSize * 3),
+                state = rememberLazyGridState().also { state = it },
+                verticalArrangement = Arrangement.spacedBy(spacingSize),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset)
+                .isEqualTo(itemSizePx + spacingSizePx / 2)
+        }
+    }
+
+    @Test
+    fun horizontal_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyHorizontalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.size(itemSize * 3),
+                state = rememberLazyGridState().also { state = it },
+                horizontalArrangement = Arrangement.spacedBy(spacingSize)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun horizontal_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyHorizontalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.size(itemSize * 3),
+                state = rememberLazyGridState().also { state = it },
+                horizontalArrangement = Arrangement.spacedBy(spacingSize)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset)
+                .isEqualTo(itemSizePx + spacingSizePx / 2)
+        }
+    }
+
+    // with reverseLayout == true
+
+    @Test
+    fun vertical_defaultArrangementIsBottomWithReverseLayout() {
+        rule.setContent {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                reverseLayout = true,
+                modifier = Modifier.size(containerSize)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+    }
+
+    @Test
+    fun horizontal_defaultArrangementIsEndWithReverseLayout() {
+        rule.setContent {
+            TvLazyHorizontalGrid(
+                TvGridCells.Fixed(1),
+                reverseLayout = true,
+                modifier = Modifier.requiredSize(containerSize)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(
+            Arrangement.End, LayoutDirection.Ltr, reverseLayout = true
+        )
+    }
+
+    @Test
+    fun vertical_whenArrangementChanges() {
+        var arrangement by mutableStateOf(Arrangement.Top)
+        rule.setContent {
+            TvLazyVerticalGrid(
+                modifier = Modifier.requiredSize(containerSize),
+                verticalArrangement = arrangement,
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Top)
+
+        rule.runOnIdle {
+            arrangement = Arrangement.Bottom
+        }
+
+        assertArrangementForTwoItems(Arrangement.Bottom)
+    }
+
+    @Test
+    fun horizontal_whenArrangementChanges() {
+        var arrangement by mutableStateOf(Arrangement.Start)
+        rule.setContent {
+            TvLazyHorizontalGrid(
+                TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(containerSize),
+                horizontalArrangement = arrangement
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+
+        rule.runOnIdle {
+            arrangement = Arrangement.End
+        }
+
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+    }
+
+    fun composeVerticalGridWith(arrangement: Arrangement.Vertical) {
+        rule.setContent {
+            TvLazyVerticalGrid(
+                verticalArrangement = arrangement,
+                modifier = Modifier.requiredSize(containerSize),
+                columns = TvGridCells.Fixed(1)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+    }
+
+    fun composeHorizontalWith(
+        arrangement: Arrangement.Horizontal,
+        layoutDirection: LayoutDirection
+    ) {
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                TvLazyHorizontalGrid(
+                    horizontalArrangement = arrangement,
+                    modifier = Modifier.requiredSize(containerSize),
+                    rows = TvGridCells.Fixed(1)
+                ) {
+                    items(2) {
+                        Item(it)
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        require(index < 2)
+        val size = if (index == 0) itemSize else smallerItemSize
+        Box(Modifier.requiredSize(size).testTag(index.toString()))
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Vertical,
+        reverseLayout: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) {
+                val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+                if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+            }
+            val outPositions = IntArray(2) { 0 }
+            with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+                rule.onNodeWithTag("$realIndex")
+                    .assertTopPositionInRootIsEqualTo(position.toDp())
+            }
+        }
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Horizontal,
+        layoutDirection: LayoutDirection,
+        reverseLayout: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) {
+                val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+                if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+            }
+            val outPositions = IntArray(2) { 0 }
+            with(arrangement) {
+                arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
+            }
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+                val expectedPosition = position.toDp()
+                rule.onNodeWithTag("$realIndex")
+                    .assertLeftPositionInRootIsEqualTo(expectedPosition)
+            }
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
new file mode 100644
index 0000000..0f444fa
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyCustomKeysTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val itemSize = with(rule.density) {
+        100.toDp()
+    }
+    val columns = 2
+
+    @Test
+    fun itemsWithKeysAreLaidOutCorrectly() {
+        val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item("${it.id}")
+                }
+            }
+        }
+
+        assertItems("0", "1", "2")
+    }
+
+    @Test
+    fun removing_statesAreMoved() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2])
+        }
+
+        assertItems("0", "2")
+    }
+
+    @Test
+    fun reordering_statesAreMoved_list() {
+        testReordering { grid ->
+            items(grid, key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_list_indexed() {
+        testReordering { grid ->
+            itemsIndexed(grid, key = { _, item -> item.id }) { _, item ->
+                Item(remember { "${item.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_array() {
+        testReordering { grid ->
+            val array = grid.toTypedArray()
+            items(array, key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_array_indexed() {
+        testReordering { grid ->
+            val array = grid.toTypedArray()
+            itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
+                Item(remember { "${item.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_itemsWithCount() {
+        testReordering { grid ->
+            items(grid.size, key = { grid[it].id }) {
+                Item(remember { "${grid[it].id}" })
+            }
+        }
+    }
+
+    @Test
+    fun fullyReplacingTheList() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6))
+        }
+
+        assertItems("3", "4", "5", "6")
+    }
+
+    @Test
+    fun keepingOneItem() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(1))
+        }
+
+        assertItems("1")
+    }
+
+    @Test
+    fun keepingOneItemAndAddingMore() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(1), MyClass(3))
+        }
+
+        assertItems("1", "3")
+    }
+
+    @Test
+    fun mixingKeyedItemsAndNot() {
+        testReordering { list ->
+            item {
+                Item("${list.first().id}")
+            }
+            items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun updatingTheDataSetIsCorrectlyApplied() {
+        val state = mutableStateOf(emptyList<Int>())
+
+        rule.setContent {
+            LaunchedEffect(Unit) {
+                state.value = listOf(4, 1, 3)
+            }
+
+            val list = state.value
+
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.fillMaxSize()) {
+                items(list, key = { it }) {
+                    Item(it.toString())
+                }
+            }
+        }
+
+        assertItems("4", "1", "3")
+
+        rule.runOnIdle {
+            state.value = listOf(2, 4, 6, 1, 3, 5)
+        }
+
+        assertItems("2", "4", "6", "1", "3", "5")
+    }
+
+    @Test
+    fun reordering_usingMutableStateListOf() {
+        val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list.add(list.removeAt(1))
+        }
+
+        assertItems("0", "2", "1")
+    }
+
+    @Test
+    fun keysInLazyListItemInfoAreCorrect() {
+        val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), state = state) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(0, 1, 2))
+        }
+    }
+
+    @Test
+    fun keysInLazyListItemInfoAreCorrectAfterReordering() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(columns = TvGridCells.Fixed(columns), state = state) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2], list[1])
+        }
+
+        rule.runOnIdle {
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(0, 2, 1))
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
+        var list by mutableStateOf((10..15).toList())
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+                items(list) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeKeepingThisItemFirst() {
+        var list by mutableStateOf((10..15).toList())
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(10, 11, 12, 13, 14, 15))
+        }
+    }
+
+    @Test
+    fun addingItemsRightAfterKeepingThisItemFirst() {
+        var list by mutableStateOf((0..5).toList() + (10..15).toList())
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState(5)
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(4, 5, 6, 7, 8, 9))
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
+        var list by mutableStateOf((10..30).toList())
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState(10) // key 20 is the first item
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..30).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(20)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(20, 21, 22, 23, 24, 25))
+        }
+    }
+
+    @Test
+    fun removingTheCurrentItemMaintainsTheIndex() {
+        var list by mutableStateOf((0..20).toList())
+        lateinit var state: TvLazyGridState
+
+        rule.setContent {
+            state = rememberLazyGridState(8)
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..20) - 8
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(8)
+            assertThat(state.visibleKeys).isEqualTo(listOf(9, 10, 11, 12, 13, 14))
+        }
+    }
+
+    private fun testReordering(content: TvLazyGridScope.(List<MyClass>) -> Unit) {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+                content(list)
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2], list[1])
+        }
+
+        assertItems("0", "2", "1")
+    }
+
+    private fun assertItems(vararg tags: String) {
+        var currentTop = 0.dp
+        var column = 0
+        tags.forEach {
+            rule.onNodeWithTag(it)
+                .assertTopPositionInRootIsEqualTo(currentTop)
+                .assertHeightIsEqualTo(itemSize)
+            ++column
+            if (column == columns) {
+                currentTop += itemSize
+                column = 0
+            }
+        }
+    }
+
+    @Composable
+    private fun Item(tag: String) {
+        Spacer(
+            Modifier.testTag(tag).size(itemSize)
+        )
+    }
+
+    private class MyClass(val id: Int)
+}
+
+val TvLazyGridState.visibleKeys: List<Any> get() = layoutInfo.visibleItemsInfo.map { it.key }
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
new file mode 100644
index 0000000..d966436
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -0,0 +1,1348 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.isSpecified
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runners.Parameterized
+import kotlin.math.roundToInt
+import kotlinx.coroutines.runBlocking
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyGridAnimateItemPlacementTest(private val config: Config) {
+
+    private val isVertical: Boolean get() = config.isVertical
+    private val reverseLayout: Boolean get() = config.reverseLayout
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val itemSize: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private val itemSize2: Int = 30
+    private var itemSize2Dp: Dp = Dp.Infinity
+    private val itemSize3: Int = 20
+    private var itemSize3Dp: Dp = Dp.Infinity
+    private val containerSize: Int = itemSize * 5
+    private var containerSizeDp: Dp = Dp.Infinity
+    private val spacing: Int = 10
+    private var spacingDp: Dp = Dp.Infinity
+    private val itemSizePlusSpacing = itemSize + spacing
+    private var itemSizePlusSpacingDp = Dp.Infinity
+    private lateinit var state: TvLazyGridState
+
+    @Before
+    fun before() {
+        rule.mainClock.autoAdvance = false
+        with(rule.density) {
+            itemSizeDp = itemSize.toDp()
+            itemSize2Dp = itemSize2.toDp()
+            itemSize3Dp = itemSize3.toDp()
+            containerSizeDp = containerSize.toDp()
+            spacingDp = spacing.toDp()
+            itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
+        }
+    }
+
+    @Test
+    fun reorderTwoItems() {
+        var list by mutableStateOf(listOf(0, 1))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(0, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(1, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+                1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun reorderTwoByTwoItems() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyGrid(2) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(3, 2, 1, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            val increasing = 0 + (itemSize * fraction).roundToInt()
+            val decreasing = itemSize - (itemSize * fraction).roundToInt()
+            assertPositions(
+                0 to AxisIntOffset(increasing, increasing),
+                1 to AxisIntOffset(decreasing, increasing),
+                2 to AxisIntOffset(increasing, decreasing),
+                3 to AxisIntOffset(decreasing, decreasing),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun reorderTwoItems_layoutInfoHasFinalPositions() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyGrid(2) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertLayoutInfoPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(3, 2, 1, 0)
+        }
+
+        onAnimationFrame {
+            // fraction doesn't affect the offsets in layout info
+            assertLayoutInfoPositions(
+                3 to AxisIntOffset(0, 0),
+                2 to AxisIntOffset(itemSize, 0),
+                1 to AxisIntOffset(0, itemSize),
+                0 to AxisIntOffset(itemSize, itemSize)
+            )
+        }
+    }
+
+    @Test
+    fun reorderFirstAndLastItems() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(0, itemSize),
+            2 to AxisIntOffset(0, itemSize * 2),
+            3 to AxisIntOffset(0, itemSize * 3),
+            4 to AxisIntOffset(0, itemSize * 4)
+        )
+
+        rule.runOnIdle {
+            list = listOf(4, 1, 2, 3, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, 0 + (itemSize * 4 * fraction).roundToInt()),
+                1 to AxisIntOffset(0, itemSize),
+                2 to AxisIntOffset(0, itemSize * 2),
+                3 to AxisIntOffset(0, itemSize * 3),
+                4 to AxisIntOffset(0, itemSize * 4 - (itemSize * 4 * fraction).roundToInt()),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveFirstItemToEndCausingAllItemsToAnimate() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        rule.setContent {
+            LazyGrid(2) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize),
+            4 to AxisIntOffset(0, itemSize * 2),
+            5 to AxisIntOffset(itemSize, itemSize * 2)
+        )
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 5, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            val increasingX = 0 + (itemSize * fraction).roundToInt()
+            val decreasingX = itemSize - (itemSize * fraction).roundToInt()
+            assertPositions(
+                0 to AxisIntOffset(increasingX, 0 + (itemSize * 2 * fraction).roundToInt()),
+                1 to AxisIntOffset(decreasingX, 0),
+                2 to AxisIntOffset(increasingX, itemSize - (itemSize * fraction).roundToInt()),
+                3 to AxisIntOffset(decreasingX, itemSize),
+                4 to AxisIntOffset(increasingX, itemSize * 2 - (itemSize * fraction).roundToInt()),
+                5 to AxisIntOffset(decreasingX, itemSize * 2),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun itemSizeChangeAnimatesNextItems() {
+        var height by mutableStateOf(itemSizeDp)
+        rule.setContent {
+            LazyGrid(1, minSize = itemSizeDp * 5, maxSize = itemSizeDp * 5) {
+                items(listOf(0, 1, 2, 3), key = { it }) {
+                    Item(it, height = if (it == 1) height else itemSizeDp)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            height = itemSizeDp * 2
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisSizeIsEqualTo(height)
+
+        onAnimationFrame { fraction ->
+            if (!reverseLayout) {
+                assertPositions(
+                    0 to AxisIntOffset(0, 0),
+                    1 to AxisIntOffset(0, itemSize),
+                    2 to AxisIntOffset(0, itemSize * 2 + (itemSize * fraction).roundToInt()),
+                    3 to AxisIntOffset(0, itemSize * 3 + (itemSize * fraction).roundToInt()),
+                    fraction = fraction,
+                    autoReverse = false
+                )
+            } else {
+                assertPositions(
+                    3 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                    2 to AxisIntOffset(0, itemSize * 2 - (itemSize * fraction).roundToInt()),
+                    1 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+                    0 to AxisIntOffset(0, itemSize * 4),
+                    fraction = fraction,
+                    autoReverse = false
+                )
+            }
+        }
+    }
+
+    @Test
+    fun onlyItemsWithModifierAnimates() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, itemSize * 4),
+                1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                2 to AxisIntOffset(0, itemSize),
+                3 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+                4 to AxisIntOffset(0, itemSize * 3),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animationsWithDifferentDurations() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    val duration = if (it == 1 || it == 3) Duration * 2 else Duration
+                    Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 0)
+        }
+
+        onAnimationFrame(duration = Duration * 2) { fraction ->
+            val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
+            assertPositions(
+                0 to AxisIntOffset(0, 0 + (itemSize * 4 * shorterAnimFraction).roundToInt()),
+                1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                2 to AxisIntOffset(0, itemSize * 2 - (itemSize * shorterAnimFraction).roundToInt()),
+                3 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+                4 to AxisIntOffset(0, itemSize * 4 - (itemSize * shorterAnimFraction).roundToInt()),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun multipleChildrenPerItem() {
+        var list by mutableStateOf(listOf(0, 2))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it)
+                    Item(it + 1)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(0, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(0, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(2, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+                1 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+                2 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                3 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun multipleChildrenPerItemSomeDoNotAnimate() {
+        var list by mutableStateOf(listOf(0, 2))
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it)
+                    Item(it + 1, animSpec = null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(2, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+                1 to AxisIntOffset(0, itemSize),
+                2 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                3 to AxisIntOffset(0, 0),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animateArrangementChange() {
+        var arrangement by mutableStateOf(Arrangement.Center)
+        rule.setContent {
+            LazyGrid(
+                1,
+                arrangement = arrangement,
+                minSize = itemSizeDp * 5,
+                maxSize = itemSizeDp * 5
+            ) {
+                items(listOf(1, 2, 3), key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            1 to AxisIntOffset(0, itemSize),
+            2 to AxisIntOffset(0, itemSize * 2),
+            3 to AxisIntOffset(0, itemSize * 3),
+        )
+
+        rule.runOnIdle {
+            arrangement = Arrangement.SpaceBetween
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+                2 to AxisIntOffset(0, itemSize * 2),
+                3 to AxisIntOffset(0, itemSize * 3 + (itemSize * fraction).roundToInt()),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+        rule.setContent {
+            LazyGrid(2, maxSize = itemSizeDp * 3) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize),
+            4 to AxisIntOffset(0, itemSize * 2),
+            5 to AxisIntOffset(itemSize, itemSize * 2)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset = AxisIntOffset(itemSize, 0 + (itemSize * 4 * fraction).roundToInt())
+            val item8Offset =
+                AxisIntOffset(itemSize, itemSize * 4 - (itemSize * 4 * fraction).roundToInt())
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                if (item1Offset.mainAxis < itemSize * 3) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(2 to AxisIntOffset(0, itemSize))
+                add(3 to AxisIntOffset(itemSize, itemSize))
+                add(4 to AxisIntOffset(0, itemSize * 2))
+                add(5 to AxisIntOffset(itemSize, itemSize * 2))
+                if (item8Offset.mainAxis < itemSize * 3) {
+                    add(8 to item8Offset)
+                } else {
+                    rule.onNodeWithTag("8").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+        rule.setContent {
+            LazyGrid(2, maxSize = itemSizeDp * 3, startIndex = 6) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            6 to AxisIntOffset(0, 0),
+            7 to AxisIntOffset(itemSize, 0),
+            8 to AxisIntOffset(0, itemSize),
+            9 to AxisIntOffset(itemSize, itemSize),
+            10 to AxisIntOffset(0, itemSize * 2),
+            11 to AxisIntOffset(itemSize, itemSize * 2)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+        }
+
+        onAnimationFrame { fraction ->
+            val item8Offset = AxisIntOffset(0, itemSize - (itemSize * 4 * fraction).roundToInt())
+            val item1Offset = AxisIntOffset(
+                0,
+                itemSize * -3 + (itemSize * 4 * fraction).roundToInt()
+            )
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                if (item1Offset.mainAxis > -itemSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(6 to AxisIntOffset(0, 0))
+                add(7 to AxisIntOffset(itemSize, 0))
+                if (item8Offset.mainAxis > -itemSize) {
+                    add(8 to item8Offset)
+                } else {
+                    rule.onNodeWithTag("8").assertIsNotDisplayed()
+                }
+                add(9 to AxisIntOffset(itemSize, itemSize))
+                add(10 to AxisIntOffset(0, itemSize * 2))
+                add(11 to AxisIntOffset(itemSize, itemSize * 2))
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
+        rule.setContent {
+            LazyGrid(2, arrangement = Arrangement.spacedBy(spacingDp)) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 5, 6, 7, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            val increasingX = (fraction * itemSize).roundToInt()
+            val decreasingX = (itemSize - itemSize * fraction).roundToInt()
+            assertPositions(
+                0 to AxisIntOffset(increasingX, (itemSizePlusSpacing * 3 * fraction).roundToInt()),
+                1 to AxisIntOffset(decreasingX, 0),
+                2 to AxisIntOffset(
+                    increasingX,
+                    itemSizePlusSpacing - (itemSizePlusSpacing * fraction).roundToInt()
+                ),
+                3 to AxisIntOffset(decreasingX, itemSizePlusSpacing),
+                4 to AxisIntOffset(
+                    increasingX,
+                    itemSizePlusSpacing * 2 - (itemSizePlusSpacing * fraction).roundToInt()
+                ),
+                5 to AxisIntOffset(decreasingX, itemSizePlusSpacing * 2),
+                6 to AxisIntOffset(
+                    increasingX,
+                    itemSizePlusSpacing * 3 - (itemSizePlusSpacing * fraction).roundToInt()
+                ),
+                7 to AxisIntOffset(decreasingX, itemSizePlusSpacing * 3),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
+        rule.setContent {
+            LazyGrid(
+                2,
+                maxSize = itemSizeDp * 3 + spacingDp * 2,
+                arrangement = Arrangement.spacedBy(spacingDp)
+            ) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSizePlusSpacing),
+            3 to AxisIntOffset(itemSize, itemSizePlusSpacing),
+            4 to AxisIntOffset(0, itemSizePlusSpacing * 2),
+            5 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset = AxisIntOffset(
+                itemSize,
+                (itemSizePlusSpacing * 4 * fraction).roundToInt()
+            )
+            val item8Offset = AxisIntOffset(
+                itemSize,
+                itemSizePlusSpacing * 4 - (itemSizePlusSpacing * 4 * fraction).roundToInt()
+            )
+            val screenSize = itemSize * 3 + spacing * 2
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                if (item1Offset.mainAxis < screenSize) {
+                    add(1 to item1Offset)
+                }
+                add(2 to AxisIntOffset(0, itemSizePlusSpacing))
+                add(3 to AxisIntOffset(itemSize, itemSizePlusSpacing))
+                add(4 to AxisIntOffset(0, itemSizePlusSpacing * 2))
+                add(5 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2))
+                if (item8Offset.mainAxis < screenSize) {
+                    add(8 to item8Offset)
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+        rule.setContent {
+            LazyGrid(
+                2,
+                maxSize = itemSizeDp * 3 + spacingDp * 2,
+                arrangement = Arrangement.spacedBy(spacingDp),
+                startIndex = 4
+            ) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            4 to AxisIntOffset(0, 0),
+            5 to AxisIntOffset(itemSize, 0),
+            6 to AxisIntOffset(0, itemSizePlusSpacing),
+            7 to AxisIntOffset(itemSize, itemSizePlusSpacing),
+            8 to AxisIntOffset(0, itemSizePlusSpacing * 2),
+            9 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset = AxisIntOffset(
+                0,
+                itemSizePlusSpacing * -2 + (itemSizePlusSpacing * 4 * fraction).roundToInt()
+            )
+            val item8Offset = AxisIntOffset(
+                0,
+                itemSizePlusSpacing * 2 - (itemSizePlusSpacing * 4 * fraction).roundToInt()
+            )
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                if (item1Offset.mainAxis > -itemSize) {
+                    add(1 to item1Offset)
+                }
+                add(4 to AxisIntOffset(0, 0))
+                add(5 to AxisIntOffset(itemSize, 0))
+                add(6 to AxisIntOffset(0, itemSizePlusSpacing))
+                add(7 to AxisIntOffset(itemSize, itemSizePlusSpacing))
+                if (item8Offset.mainAxis > -itemSize) {
+                    add(8 to item8Offset)
+                }
+                add(9 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2))
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds_differentSizes() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+        rule.setContent {
+            LazyGrid(2, maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 6) {
+                items(list, key = { it }) {
+                    val height = when (it) {
+                        2 -> itemSize3Dp
+                        3 -> itemSize3Dp / 2
+                        6 -> itemSize2Dp
+                        7 -> itemSize2Dp / 2
+                        else -> {
+                            if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
+                        }
+                    }
+                    Item(it, height = height)
+                }
+            }
+        }
+
+        val line3Size = itemSize2
+        val line4Size = itemSize
+        assertPositions(
+            6 to AxisIntOffset(0, 0),
+            7 to AxisIntOffset(itemSize, 0),
+            8 to AxisIntOffset(0, line3Size),
+            9 to AxisIntOffset(itemSize, line3Size),
+            10 to AxisIntOffset(0, line3Size + line4Size),
+            11 to AxisIntOffset(itemSize, line3Size + line4Size)
+        )
+
+        rule.runOnIdle {
+            // swap 8 and 2
+            list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11)
+        }
+
+        onAnimationFrame { fraction ->
+            rule.onNodeWithTag("4").assertDoesNotExist()
+            rule.onNodeWithTag("5").assertDoesNotExist()
+            // items 4,5 were between lines 1 and 3 but we don't compose them and don't know the
+            // real size, so we use an average size.
+            val line2Size = (itemSize + itemSize2 + itemSize3) / 3
+            val line1Size = itemSize3 /* the real size of the item 2 */
+            val startItem2Offset = -line1Size - line2Size
+            val item2Offset =
+                startItem2Offset + ((itemSize2 - startItem2Offset) * fraction).roundToInt()
+            val endItem8Offset = -line2Size - itemSize
+            val item8Offset = line3Size - ((line3Size - endItem8Offset) * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                if (item8Offset > -line4Size) {
+                    add(8 to AxisIntOffset(0, item8Offset))
+                } else {
+                    rule.onNodeWithTag("8").assertIsNotDisplayed()
+                }
+                add(6 to AxisIntOffset(0, 0))
+                add(7 to AxisIntOffset(itemSize, 0))
+                if (item2Offset > -line1Size) {
+                    add(2 to AxisIntOffset(0, item2Offset))
+                } else {
+                    rule.onNodeWithTag("2").assertIsNotDisplayed()
+                }
+                add(9 to AxisIntOffset(itemSize, line3Size))
+                add(10 to AxisIntOffset(
+                    0,
+                    line3Size + line4Size - ((itemSize - itemSize3) * fraction).roundToInt()
+                ))
+                add(11 to AxisIntOffset(
+                    itemSize,
+                    line3Size + line4Size - ((itemSize - itemSize3) * fraction).roundToInt()
+                ))
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+        val gridSize = itemSize2 + itemSize3 + itemSize - 1
+        val gridSizeDp = with(rule.density) { gridSize.toDp() }
+        rule.setContent {
+            LazyGrid(2, maxSize = gridSizeDp) {
+                items(list, key = { it }) {
+                    val height = when (it) {
+                        0 -> itemSize2Dp
+                        8 -> itemSize3Dp
+                        else -> {
+                            if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
+                        }
+                    }
+                    Item(it, height = height)
+                }
+            }
+        }
+
+        val line0Size = itemSize2
+        val line1Size = itemSize
+        assertPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, line0Size),
+            3 to AxisIntOffset(itemSize, line0Size),
+            4 to AxisIntOffset(0, line0Size + line1Size),
+            5 to AxisIntOffset(itemSize, line0Size + line1Size),
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11)
+        }
+
+        onAnimationFrame { fraction ->
+            val line2Size = itemSize
+            val line4Size = itemSize3
+            // line 3 was between 2 and 4 but we don't compose it and don't know the real size,
+            // so we use an average size.
+            val line3Size = (itemSize + itemSize2 + itemSize3) / 3
+            val startItem8Offset = line0Size + line1Size + line2Size + line3Size
+            val endItem2Offset = line0Size + line4Size + line2Size + line3Size
+            val item2Offset =
+                line0Size + ((endItem2Offset - line0Size) * fraction).roundToInt()
+            val item8Offset =
+                startItem8Offset - ((startItem8Offset - line0Size) * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                add(1 to AxisIntOffset(itemSize, 0))
+                if (item8Offset < gridSize) {
+                    add(8 to AxisIntOffset(0, item8Offset))
+                } else {
+                    // rule.onNodeWithTag("8").assertIsNotDisplayed()
+                }
+                add(3 to AxisIntOffset(itemSize, line0Size))
+                add(4 to AxisIntOffset(
+                    0,
+                    line0Size + line1Size - ((line1Size - line4Size) * fraction).roundToInt()
+                ))
+                add(5 to AxisIntOffset(
+                    itemSize,
+                    line0Size + line1Size - ((line1Size - line4Size) * fraction).roundToInt()
+                ))
+                if (item2Offset < gridSize) {
+                    add(2 to AxisIntOffset(0, item2Offset))
+                } else {
+                    // rule.onNodeWithTag("2").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    // @Test
+    // fun animateAlignmentChange() {
+    //     var alignment by mutableStateOf(CrossAxisAlignment.End)
+    //     rule.setContent {
+    //         LazyList(
+    //             crossAxisAlignment = alignment,
+    //             crossAxisSize = itemSizeDp
+    //         ) {
+    //             items(listOf(1, 2, 3), key = { it }) {
+    //                 val crossAxisSize =
+    //                     if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+    //                 Item(it, crossAxisSize = crossAxisSize)
+    //             }
+    //         }
+    //     }
+
+    //     val item2Start = itemSize - itemSize2
+    //     val item3Start = itemSize - itemSize3
+    //     assertPositions(
+    //         1 to 0,
+    //         2 to itemSize,
+    //         3 to itemSize * 2,
+    //         crossAxis = listOf(
+    //             1 to 0,
+    //             2 to item2Start,
+    //             3 to item3Start,
+    //         )
+    //     )
+
+    //     rule.runOnIdle {
+    //         alignment = CrossAxisAlignment.Center
+    //     }
+    //     rule.mainClock.advanceTimeByFrame()
+
+    //     val item2End = itemSize / 2 - itemSize2 / 2
+    //     val item3End = itemSize / 2 - itemSize3 / 2
+    //     onAnimationFrame { fraction ->
+    //         assertPositions(
+    //             1 to 0,
+    //             2 to itemSize,
+    //             3 to itemSize * 2,
+    //             crossAxis = listOf(
+    //                 1 to 0,
+    //                 2 to item2Start + ((item2End - item2Start) * fraction).roundToInt(),
+    //                 3 to item3Start + ((item3End - item3Start) * fraction).roundToInt(),
+    //             ),
+    //             fraction = fraction
+    //         )
+    //     }
+    // }
+
+    // @Test
+    // fun animateAlignmentChange_multipleChildrenPerItem() {
+    //     var alignment by mutableStateOf(CrossAxisAlignment.Start)
+    //     rule.setContent {
+    //         LazyList(
+    //             crossAxisAlignment = alignment,
+    //             crossAxisSize = itemSizeDp * 2
+    //         ) {
+    //             items(1) {
+    //                 listOf(1, 2, 3).forEach {
+    //                     val crossAxisSize =
+    //                         if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+    //                     Item(it, crossAxisSize = crossAxisSize)
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.runOnIdle {
+    //         alignment = CrossAxisAlignment.End
+    //     }
+    //     rule.mainClock.advanceTimeByFrame()
+
+    //     val containerSize = itemSize * 2
+    //     onAnimationFrame { fraction ->
+    //         assertPositions(
+    //             1 to 0,
+    //             2 to itemSize,
+    //             3 to itemSize * 2,
+    //             crossAxis = listOf(
+    //                 1 to ((containerSize - itemSize) * fraction).roundToInt(),
+    //                 2 to ((containerSize - itemSize2) * fraction).roundToInt(),
+    //                 3 to ((containerSize - itemSize3) * fraction).roundToInt()
+    //             ),
+    //             fraction = fraction
+    //         )
+    //     }
+    // }
+
+    // @Test
+    // fun animateAlignmentChange_rtl() {
+    //     // this test is not applicable to LazyRow
+    //     assumeTrue(isVertical)
+
+    //     var alignment by mutableStateOf(CrossAxisAlignment.End)
+    //     rule.setContent {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyList(
+    //                 crossAxisAlignment = alignment,
+    //                 crossAxisSize = itemSizeDp
+    //             ) {
+    //                 items(listOf(1, 2, 3), key = { it }) {
+    //                     val crossAxisSize =
+    //                         if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+    //                     Item(it, crossAxisSize = crossAxisSize)
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     assertPositions(
+    //         1 to 0,
+    //         2 to itemSize,
+    //         3 to itemSize * 2,
+    //         crossAxis = listOf(
+    //             1 to 0,
+    //             2 to 0,
+    //             3 to 0,
+    //         )
+    //     )
+
+    //     rule.runOnIdle {
+    //         alignment = CrossAxisAlignment.Center
+    //     }
+    //     rule.mainClock.advanceTimeByFrame()
+
+    //     onAnimationFrame { fraction ->
+    //         assertPositions(
+    //             1 to 0,
+    //             2 to itemSize,
+    //             3 to itemSize * 2,
+    //             crossAxis = listOf(
+    //                 1 to 0,
+    //                 2 to ((itemSize / 2 - itemSize2 / 2) * fraction).roundToInt(),
+    //                 3 to ((itemSize / 2 - itemSize3 / 2) * fraction).roundToInt(),
+    //             ),
+    //             fraction = fraction
+    //         )
+    //     }
+    // }
+
+    @Test
+    fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        val rawStartPadding = 8
+        val rawEndPadding = 12
+        val (startPaddingDp, endPaddingDp) = with(rule.density) {
+            rawStartPadding.toDp() to rawEndPadding.toDp()
+        }
+        rule.setContent {
+            LazyGrid(1, startPadding = startPaddingDp, endPadding = endPaddingDp) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
+        assertPositions(
+            0 to AxisIntOffset(0, startPadding),
+            1 to AxisIntOffset(0, startPadding + itemSize),
+            2 to AxisIntOffset(0, startPadding + itemSize * 2),
+            3 to AxisIntOffset(0, startPadding + itemSize * 3),
+            4 to AxisIntOffset(0, startPadding + itemSize * 4),
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 4, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, startPadding),
+                1 to AxisIntOffset(
+                    0,
+                    startPadding + itemSize + (itemSize * 3 * fraction).roundToInt()
+                ),
+                2 to AxisIntOffset(
+                    0,
+                    startPadding + itemSize * 2 - (itemSize * fraction).roundToInt()
+                ),
+                3 to AxisIntOffset(
+                    0,
+                    startPadding + itemSize * 3 - (itemSize * fraction).roundToInt()
+                ),
+                4 to AxisIntOffset(
+                    0,
+                    startPadding + itemSize * 4 - (itemSize * fraction).roundToInt()
+                ),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+
+        var measurePasses = 0
+        rule.setContent {
+            LazyGrid(1) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+            LaunchedEffect(Unit) {
+                snapshotFlow { state.layoutInfo }
+                    .collect {
+                        measurePasses++
+                    }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(4, 1, 2, 3, 0)
+        }
+
+        var startMeasurePasses = Int.MIN_VALUE
+        onAnimationFrame { fraction ->
+            if (fraction == 0f) {
+                startMeasurePasses = measurePasses
+            }
+        }
+        rule.mainClock.advanceTimeByFrame()
+        // new layoutInfo is produced on every remeasure of Lazy lists.
+        // but we want to avoid remeasuring and only do relayout on each animation frame.
+        // two extra measures are possible as we switch inProgress flag.
+        assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
+    }
+
+    @Test
+    fun noAnimationWhenScrollOtherPosition() {
+        rule.setContent {
+            LazyGrid(1, maxSize = itemSizeDp * 3) {
+                items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0, itemSize / 2)
+            }
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to AxisIntOffset(0, -itemSize / 2),
+                1 to AxisIntOffset(0, itemSize / 2),
+                2 to AxisIntOffset(0, itemSize * 3 / 2),
+                3 to AxisIntOffset(0, itemSize * 5 / 2),
+                fraction = fraction
+            )
+        }
+    }
+
+    private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
+        if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
+
+    private val IntOffset.mainAxis: Int get() = if (isVertical) y else x
+
+    private fun assertPositions(
+        vararg expected: Pair<Any, IntOffset>,
+        crossAxis: List<Pair<Any, Int>>? = null,
+        fraction: Float? = null,
+        autoReverse: Boolean = reverseLayout
+    ) {
+        with(rule.density) {
+            val actual = expected.map {
+                val actualOffset = rule.onNodeWithTag(it.first.toString())
+                    .getUnclippedBoundsInRoot().let { bounds ->
+                        IntOffset(
+                            if (bounds.left.isSpecified) bounds.left.roundToPx() else Int.MIN_VALUE,
+                            if (bounds.top.isSpecified) bounds.top.roundToPx() else Int.MIN_VALUE
+                        )
+                    }
+                it.first to actualOffset
+            }
+            val subject = if (fraction == null) {
+                assertThat(actual)
+            } else {
+                assertWithMessage("Fraction=$fraction").that(actual)
+            }
+            subject.isEqualTo(
+                listOf(*expected).let { list ->
+                    if (!autoReverse) {
+                        list
+                    } else {
+                        val containerBounds = rule.onNodeWithTag(ContainerTag).getBoundsInRoot()
+                        val containerSize = with(rule.density) {
+                            IntSize(
+                                containerBounds.width.roundToPx(),
+                                containerBounds.height.roundToPx()
+                            )
+                        }
+                        list.map {
+                            val itemSize = rule.onNodeWithTag(it.first.toString())
+                                .getUnclippedBoundsInRoot().let {
+                                    IntSize(it.width.roundToPx(), it.height.roundToPx())
+                                }
+                            it.first to
+                                IntOffset(
+                                    if (isVertical) {
+                                        it.second.x
+                                    } else {
+                                        containerSize.width - itemSize.width - it.second.x
+                                    },
+                                    if (!isVertical) {
+                                        it.second.y
+                                    } else {
+                                        containerSize.height - itemSize.height - it.second.y
+                                    }
+                                )
+                        }
+                    }
+                }
+            )
+            if (crossAxis != null) {
+                val actualCross = expected.map {
+                    val actualOffset = rule.onNodeWithTag(it.first.toString())
+                        .getUnclippedBoundsInRoot().let { bounds ->
+                            val offset = if (isVertical) bounds.left else bounds.top
+                            if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+                        }
+                    it.first to actualOffset
+                }
+                assertWithMessage(
+                    "CrossAxis" + if (fraction != null) "for fraction=$fraction" else ""
+                )
+                    .that(actualCross)
+                    .isEqualTo(crossAxis)
+            }
+        }
+    }
+
+    private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, IntOffset>) {
+        rule.runOnIdle {
+            assertThat(visibleItemsOffsets).isEqualTo(listOf(*offsets))
+        }
+    }
+
+    private val visibleItemsOffsets: List<Pair<Any, IntOffset>>
+        get() = state.layoutInfo.visibleItemsInfo.map {
+            it.key to it.offset
+        }
+
+    private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+        require(duration.mod(FrameDuration) == 0L)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            onFrame(i / duration.toFloat())
+            rule.mainClock.advanceTimeBy(FrameDuration)
+            expectedTime += FrameDuration
+            assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            rule.waitForIdle()
+        }
+    }
+
+    @Composable
+    private fun LazyGrid(
+        columns: Int,
+        arrangement: Arrangement.HorizontalOrVertical? = null,
+        minSize: Dp = 0.dp,
+        maxSize: Dp = containerSizeDp,
+        startIndex: Int = 0,
+        startPadding: Dp = 0.dp,
+        endPadding: Dp = 0.dp,
+        content: TvLazyGridScope.() -> Unit
+    ) {
+        state = rememberLazyGridState(startIndex)
+        if (isVertical) {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(columns),
+                Modifier
+                    .requiredHeightIn(minSize, maxSize)
+                    .requiredWidth(itemSizeDp * columns)
+                    .testTag(ContainerTag),
+                state = state,
+                verticalArrangement = arrangement as? Arrangement.Vertical
+                    ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
+                content = content
+            )
+        } else {
+            TvLazyHorizontalGrid(
+                TvGridCells.Fixed(columns),
+                Modifier
+                    .requiredWidthIn(minSize, maxSize)
+                    .requiredHeight(itemSizeDp * columns)
+                    .testTag(ContainerTag),
+                state = state,
+                horizontalArrangement = arrangement as? Arrangement.Horizontal
+                    ?: if (!reverseLayout) Arrangement.Start else Arrangement.End,
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(start = startPadding, end = endPadding),
+                content = content
+            )
+        }
+    }
+
+    @Composable
+    private fun TvLazyGridItemScope.Item(
+        tag: Int,
+        height: Dp = itemSizeDp,
+        animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
+    ) {
+        Box(
+            Modifier
+                .then(
+                    if (isVertical) {
+                        Modifier.requiredHeight(height)
+                    } else {
+                        Modifier.requiredWidth(height)
+                    }
+                )
+                .testTag(tag.toString())
+                .then(
+                    if (animSpec != null) {
+                        Modifier.animateItemPlacement(animSpec)
+                    } else {
+                        Modifier
+                    }
+                )
+        )
+    }
+
+    private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
+        expected: Dp
+    ): SemanticsNodeInteraction {
+        return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
+    }
+
+    companion object {
+        @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),
+        )
+
+        class Config(
+            val isVertical: Boolean,
+            val reverseLayout: Boolean
+        ) {
+            override fun toString() =
+                (if (isVertical) "LazyVerticalGrid" else "LazyHorizontalGrid") +
+                    (if (reverseLayout) "(reverse)" else "")
+        }
+    }
+}
+
+private val FrameDuration = 16L
+private val Duration = 400L
+private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
+private val ContainerTag = "container"
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
new file mode 100644
index 0000000..b2e1a5a
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyGridPrefetcherTest(
+    orientation: Orientation
+) : BaseLazyGridTestWithOrientation(orientation) {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            Orientation.Vertical,
+            Orientation.Horizontal,
+        )
+    }
+
+    val itemsSizePx = 30
+    val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+    lateinit var state: TvLazyGridState
+
+    @Test
+    fun notPrefetchingForwardInitially() {
+        composeList()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun notPrefetchingBackwardInitially() {
+        composeList(firstItem = 4)
+
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAfterSmallScroll() {
+        composeList()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(4)
+
+        rule.onNodeWithTag("4")
+            .assertExists()
+        rule.onNodeWithTag("5")
+            .assertExists()
+        rule.onNodeWithTag("6")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingBackwardAfterSmallScroll() {
+        composeList(firstItem = 4, itemOffset = 10)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-5f)
+            }
+        }
+
+        waitForPrefetch(2)
+
+        rule.onNodeWithTag("2")
+            .assertExists()
+        rule.onNodeWithTag("3")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackward() {
+        composeList(firstItem = 2)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(6)
+
+        rule.onNodeWithTag("6")
+            .assertExists()
+        rule.onNodeWithTag("7")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+                state.scrollBy(-1f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+        rule.onNodeWithTag("1")
+            .assertExists()
+        rule.onNodeWithTag("6")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardTwice() {
+        composeList()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(4)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemsSizePx / 2f)
+                state.scrollBy(itemsSizePx / 2f)
+            }
+        }
+
+        waitForPrefetch(6)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("6")
+            .assertExists()
+        rule.onNodeWithTag("8")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingBackwardTwice() {
+        composeList(firstItem = 8)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-5f)
+            }
+        }
+
+        waitForPrefetch(4)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-itemsSizePx / 2f)
+                state.scrollBy(-itemsSizePx / 2f)
+            }
+        }
+
+        waitForPrefetch(2)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("2")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackwardReverseLayout() {
+        composeList(firstItem = 2, reverseLayout = true)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(6)
+
+        rule.onNodeWithTag("6")
+            .assertExists()
+        rule.onNodeWithTag("7")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+                state.scrollBy(-1f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+        rule.onNodeWithTag("1")
+            .assertExists()
+        rule.onNodeWithTag("6")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("7")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackwardWithContentPadding() {
+        val halfItemSize = itemsSizeDp / 2f
+        composeList(
+            firstItem = 4,
+            itemOffset = 5,
+            contentPadding = PaddingValues(mainAxis = halfItemSize)
+        )
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("8")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(6)
+
+        rule.onNodeWithTag("8")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+    }
+
+    @Test
+    fun disposingWhilePrefetchingScheduled() {
+        var emit = true
+        lateinit var remeasure: Remeasurement
+        rule.setContent {
+            SubcomposeLayout(
+                modifier = object : RemeasurementModifier {
+                    override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+                        remeasure = remeasurement
+                    }
+                }
+            ) { constraints ->
+                val placeable = if (emit) {
+                    subcompose(Unit) {
+                        state = rememberLazyGridState()
+                        LazyGrid(
+                            2,
+                            Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+                            state,
+                        ) {
+                            items(1000) {
+                                Spacer(
+                                    Modifier.mainAxisSize(itemsSizeDp)
+                                )
+                            }
+                        }
+                    }.first().measure(constraints)
+                } else {
+                    null
+                }
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeable?.place(0, 0)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // this will schedule the prefetching
+            runBlocking(AutoTestFrameClock()) {
+                state.scrollBy(itemsSizePx.toFloat())
+            }
+            // then we synchronously dispose LazyColumn
+            emit = false
+            remeasure.forceRemeasure()
+        }
+
+        rule.runOnIdle { }
+    }
+
+    private fun waitForPrefetch(index: Int) {
+        rule.waitUntil {
+            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+        }
+    }
+
+    private val activeNodes = mutableSetOf<Int>()
+    private val activeMeasuredNodes = mutableSetOf<Int>()
+
+    private fun composeList(
+        firstItem: Int = 0,
+        itemOffset: Int = 0,
+        reverseLayout: Boolean = false,
+        contentPadding: PaddingValues = PaddingValues(0.dp)
+    ) {
+        rule.setContent {
+            state = rememberLazyGridState(
+                initialFirstVisibleItemIndex = firstItem,
+                initialFirstVisibleItemScrollOffset = itemOffset
+            )
+            LazyGrid(
+                2,
+                Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+                state,
+                reverseLayout = reverseLayout,
+                contentPadding = contentPadding
+            ) {
+                items(100) {
+                    DisposableEffect(it) {
+                        activeNodes.add(it)
+                        onDispose {
+                            activeNodes.remove(it)
+                            activeMeasuredNodes.remove(it)
+                        }
+                    }
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(itemsSizeDp)
+                            .testTag("$it")
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                activeMeasuredNodes.add(it)
+                                layout(placeable.width, placeable.height) {
+                                    placeable.place(0, 0)
+                                }
+                            }
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
new file mode 100644
index 0000000..c9f6943
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridSlotsReuseTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val itemsSizePx = 30f
+    val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+    @Test
+    fun scroll1ItemScrolledOffItemIsKeptForReuse() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1)
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun checkMaxItemsKeptForReuse() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(DefaultMaxItemsToRetain + 1)
+            }
+        }
+
+        repeat(DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$it")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                // after this step 0 and 1 are in reusable buffer
+                state.scrollToItem(2)
+
+                // this step requires one item and will take the last item from the buffer - item
+                // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
+                state.scrollToItem(3)
+            }
+        }
+
+        // recycled
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        // in buffer
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("2")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun doMultipleScrollsOneByOne() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1) // buffer is [0]
+                state.scrollToItem(2) // 0 used, buffer is [1]
+                state.scrollToItem(3) // 1 used, buffer is [2]
+                state.scrollToItem(4) // 2 used, buffer is [3]
+            }
+        }
+
+        // recycled
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+
+        // in buffer
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollBackwardOnce() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState(10)
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(8) // buffer is [10, 11]
+            }
+        }
+
+        // in buffer
+        rule.onNodeWithTag("10")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("11")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("8")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("9")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollBackwardOneByOne() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState(10)
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(9) // buffer is [11]
+                state.scrollToItem(7) // 11 reused, buffer is [9]
+                state.scrollToItem(6) // 9 reused, buffer is [8]
+            }
+        }
+
+        // in buffer
+        rule.onNodeWithTag("8")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("7")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollingBackReusesTheSameSlot() {
+        lateinit var state: TvLazyGridState
+        var counter0 = 0
+        var counter1 = 10
+        var rememberedValue0 = -1
+        var rememberedValue1 = -1
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 1.5f),
+                state
+            ) {
+                items(100) {
+                    if (it == 0) {
+                        rememberedValue0 = remember { counter0++ }
+                    }
+                    if (it == 1) {
+                        rememberedValue1 = remember { counter1++ }
+                    }
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2) // buffer is [0, 1]
+                state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
+                .that(rememberedValue0).isEqualTo(0)
+            Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
+                .that(rememberedValue1).isEqualTo(10)
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+    }
+
+    @Test
+    fun differentContentTypes() {
+        lateinit var state: TvLazyGridState
+        val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
+        val startOfType1 = DefaultMaxItemsToRetain + 1
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
+                state
+            ) {
+                items(
+                    100,
+                    contentType = { if (it >= startOfType1) 1 else 0 }
+                ) {
+                    Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+                }
+            }
+        }
+
+        for (i in 0 until visibleItemsCount) {
+            rule.onNodeWithTag("$i")
+                .assertIsDisplayed()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(visibleItemsCount)
+            }
+        }
+
+        rule.onNodeWithTag("$visibleItemsCount")
+            .assertIsDisplayed()
+
+        // [DefaultMaxItemsToRetain] items of type 0 are left for reuse
+        for (i in 0 until DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$i")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+            .assertDoesNotExist()
+
+        // and 7 items of type 1
+        for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$i")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun differentTypesFromDifferentItemCalls() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.height(itemsSizeDp * 2.5f),
+                state
+            ) {
+                val content = @Composable { tag: String ->
+                    Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
+                }
+                item(contentType = "not-to-reuse-0") {
+                    content("0")
+                }
+                item(contentType = "reuse") {
+                    content("1")
+                }
+                items(
+                    List(100) { it + 2 },
+                    contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }) {
+                    content("$it")
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2)
+                // now items 0 and 1 are put into reusables
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(9)
+                // item 10 should reuse slot 1
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("9")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("10")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("11")
+            .assertIsDisplayed()
+    }
+}
+
+private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt
new file mode 100644
index 0000000..3d4c09e
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridSpanTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun spans() {
+        val columns = 4
+        val columnWidth = with(rule.density) { 5.toDp() }
+        val itemHeight = with(rule.density) { 10.toDp() }
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(columns),
+                modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
+            ) {
+                items(
+                    count = 6,
+                    span = { index ->
+                        when (index) {
+                            0 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+                                TvGridItemSpan(3)
+                            }
+                            1 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(1)
+                                TvGridItemSpan(1)
+                            }
+                            2 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+                                TvGridItemSpan(1)
+                            }
+                            3 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
+                                TvGridItemSpan(3)
+                            }
+                            4 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+                                TvGridItemSpan(1)
+                            }
+                            5 -> {
+                                Truth.assertThat(maxLineSpan).isEqualTo(4)
+                                Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
+                                TvGridItemSpan(1)
+                            }
+                            else -> error("Out of index span queried")
+                        }
+                    },
+                ) {
+                    Box(Modifier.height(itemHeight).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemHeight)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemHeight)
+            .assertLeftPositionInRootIsEqualTo(columnWidth)
+        rule.onNodeWithTag("4")
+            .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("5")
+            .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+            .assertLeftPositionInRootIsEqualTo(columnWidth)
+    }
+
+    @Test
+    fun spansWithHorizontalSpacing() {
+        val columns = 4
+        val columnWidth = with(rule.density) { 5.toDp() }
+        val itemHeight = with(rule.density) { 10.toDp() }
+        val spacing = with(rule.density) { 4.toDp() }
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(columns),
+                modifier = Modifier.requiredSize(
+                    columnWidth * columns + spacing * (columns - 1),
+                    itemHeight
+                ),
+                horizontalArrangement = Arrangement.spacedBy(spacing)
+            ) {
+                items(
+                    count = 2,
+                    span = { index ->
+                        when (index) {
+                            0 -> TvGridItemSpan(1)
+                            1 -> TvGridItemSpan(3)
+                            else -> error("Out of index span queried")
+                        }
+                    }
+                ) {
+                    Box(Modifier.height(itemHeight).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(columnWidth)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(columnWidth + spacing)
+            .assertWidthIsEqualTo(columnWidth * 3 + spacing * 2)
+    }
+
+    @Test
+    fun spansMultipleBlocks() {
+        val columns = 4
+        val columnWidth = with(rule.density) { 5.toDp() }
+        val itemHeight = with(rule.density) { 10.toDp() }
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(columns),
+                modifier = Modifier.requiredSize(columnWidth * columns, itemHeight)
+            ) {
+                items(
+                    count = 1,
+                    span = { index ->
+                        when (index) {
+                            0 -> TvGridItemSpan(1)
+                            else -> error("Out of index span queried")
+                        }
+                    }
+                ) {
+                    Box(Modifier.height(itemHeight).testTag("0"))
+                }
+                item(span = {
+                    if (maxCurrentLineSpan != 3) error("Wrong maxSpan")
+                    TvGridItemSpan(2)
+                }) {
+                    Box(Modifier.height(itemHeight).testTag("1"))
+                }
+                items(
+                    count = 1,
+                    span = { index ->
+                        if (maxCurrentLineSpan != 1 || index != 0) {
+                            error("Wrong span calculation parameters")
+                        }
+                        TvGridItemSpan(1)
+                    }
+                ) {
+                    if (it != 0) error("Wrong index")
+                    Box(Modifier.height(itemHeight).testTag("2"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(columnWidth)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(columnWidth)
+            .assertWidthIsEqualTo(columnWidth * 2)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
+            .assertWidthIsEqualTo(columnWidth)
+    }
+
+    @Test
+    fun spansLineBreak() {
+        val columns = 4
+        val columnWidth = with(rule.density) { 5.toDp() }
+        val itemHeight = with(rule.density) { 10.toDp() }
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(columns),
+                modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
+            ) {
+                item(span = {
+                    if (maxCurrentLineSpan != 4) error("Wrong maxSpan")
+                    TvGridItemSpan(3)
+                }) {
+                    Box(Modifier.height(itemHeight).testTag("0"))
+                }
+                items(
+                    count = 4,
+                    span = { index ->
+                        if (maxCurrentLineSpan != when (index) {
+                                0 -> 1
+                                1 -> 2
+                                2 -> 1
+                                3 -> 2
+                                else -> error("Wrong index")
+                            }
+                        ) error("Wrong maxSpan")
+                        TvGridItemSpan(listOf(2, 1, 2, 2)[index])
+                    }
+                ) {
+                    Box(Modifier.height(itemHeight).testTag((it + 1).toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(columnWidth * 3)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemHeight)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(columnWidth * 2)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemHeight)
+            .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
+            .assertWidthIsEqualTo(columnWidth)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(columnWidth * 2)
+        rule.onNodeWithTag("4")
+            .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+            .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
+            .assertWidthIsEqualTo(columnWidth * 2)
+    }
+
+    @Test
+    fun spansCalculationDoesntCrash() {
+        // regression from b/222530458
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(2),
+                state = state,
+                modifier = Modifier.size(100.dp)
+            ) {
+                repeat(100) {
+                    item(span = { TvGridItemSpan(maxLineSpan) }) {
+                        Box(Modifier.fillMaxWidth().height(1.dp))
+                    }
+                    items(10) {
+                        Box(Modifier.fillMaxWidth().height(1.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(state.layoutInfo.totalItemsCount)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt
new file mode 100644
index 0000000..4f170ec
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -0,0 +1,1070 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import android.os.Build
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyGridTest(
+    private val orientation: Orientation
+) : BaseLazyGridTestWithOrientation(orientation) {
+    private val LazyGridTag = "LazyGridTag"
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            Orientation.Vertical,
+            Orientation.Horizontal,
+        )
+    }
+
+    @Test
+    fun lazyGridShowsOneItem() {
+        val itemTestTag = "itemTestTag"
+
+        rule.setContent {
+            LazyGrid(
+                cells = 3
+            ) {
+                item {
+                    Spacer(
+                        Modifier.size(10.dp).testTag(itemTestTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(itemTestTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyGridShowsOneLine() {
+        val items = (1..5).map { it.toString() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 3,
+                modifier = Modifier.axisSize(300.dp, 100.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("5")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyGridShowsSecondLineOnScroll() {
+        val items = (1..12).map { it.toString() }
+
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 3,
+                modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag)
+            ) {
+                items(items) {
+                    Box(Modifier.mainAxisSize(101.dp).testTag(it).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("10")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("11")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("12")
+            .assertIsNotDisplayed()
+    }
+
+    @Test
+    fun lazyGridScrollHidesFirstLine() {
+        val items = (1..9).map { it.toString() }
+
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 3,
+                modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag),
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(101.dp).testTag(it).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("7")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("8")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("9")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun adaptiveLazyGridFillsAllCrossAxisSize() {
+        val items = (1..5).map { it.toString() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(130.dp),
+                modifier = Modifier.axisSize(300.dp, 100.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertCrossAxisStartPositionInRootIsEqualTo(150.dp)
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("5")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun adaptiveLazyGridAtLeastOneSlot() {
+        val items = (1..3).map { it.toString() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(301.dp),
+                modifier = Modifier.axisSize(300.dp, 100.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun adaptiveLazyGridAppliesHorizontalSpacings() {
+        val items = (1..3).map { it.toString() }
+
+        val spacing = with(rule.density) { 10.toDp() }
+        val itemSize = with(rule.density) { 100.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(itemSize),
+                modifier = Modifier.axisSize(itemSize * 3 + spacing * 2, itemSize),
+                crossAxisSpacedBy = spacing
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun adaptiveLazyGridAppliesHorizontalSpacingsWithContentPaddings() {
+        val items = (1..3).map { it.toString() }
+
+        val spacing = with(rule.density) { 8.toDp() }
+        val itemSize = with(rule.density) { 40.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(itemSize),
+                modifier = Modifier.axisSize(itemSize * 3 + spacing * 4, itemSize),
+                crossAxisSpacedBy = spacing,
+                contentPadding = PaddingValues(crossAxis = spacing)
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing * 2)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 3)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun adaptiveLazyGridAppliesVerticalSpacings() {
+        val items = (1..3).map { it.toString() }
+
+        val spacing = with(rule.density) { 4.toDp() }
+        val itemSize = with(rule.density) { 32.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(itemSize),
+                modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
+                mainAxisSpacedBy = spacing
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize + spacing)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun adaptiveLazyGridAppliesVerticalSpacingsWithContentPadding() {
+        val items = (1..3).map { it.toString() }
+
+        val spacing = with(rule.density) { 16.toDp() }
+        val itemSize = with(rule.density) { 72.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = TvGridCells.Adaptive(itemSize),
+                modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
+                mainAxisSpacedBy = spacing,
+                contentPadding = PaddingValues(mainAxis = spacing)
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing * 3 + itemSize * 2)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun fixedLazyGridAppliesVerticalSpacings() {
+        val items = (1..4).map { it.toString() }
+
+        val spacing = with(rule.density) { 24.toDp() }
+        val itemSize = with(rule.density) { 80.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
+                mainAxisSpacedBy = spacing,
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun fixedLazyGridAppliesHorizontalSpacings() {
+        val items = (1..4).map { it.toString() }
+
+        val spacing = with(rule.density) { 15.toDp() }
+        val itemSize = with(rule.density) { 30.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(itemSize * 2 + spacing, itemSize * 2),
+                crossAxisSpacedBy = spacing
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun fixedLazyGridAppliesVerticalSpacingsWithContentPadding() {
+        val items = (1..4).map { it.toString() }
+
+        val spacing = with(rule.density) { 30.toDp() }
+        val itemSize = with(rule.density) { 77.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
+                mainAxisSpacedBy = spacing,
+                contentPadding = PaddingValues(mainAxis = spacing)
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun fixedLazyGridAppliesHorizontalSpacingsWithContentPadding() {
+        val items = (1..4).map { it.toString() }
+
+        val spacing = with(rule.density) { 22.toDp() }
+        val itemSize = with(rule.density) { 44.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(itemSize * 2 + spacing * 3, itemSize * 2),
+                crossAxisSpacedBy = spacing,
+                contentPadding = PaddingValues(crossAxis = spacing)
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun usedWithArray() {
+        val items = arrayOf("1", "2", "3", "4")
+
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.crossAxisSize(itemSize * 2)
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("4")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun usedWithArrayIndexed() {
+        val items = arrayOf("1", "2", "3", "4")
+
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                Modifier.crossAxisSize(itemSize * 2)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(Modifier.mainAxisSize(itemSize).testTag("$index*$item"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0*1")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1*2")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2*3")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3*4")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun changeItemsCountAndScrollImmediately() {
+        lateinit var state: TvLazyGridState
+        var count by mutableStateOf(100)
+        val composedIndexes = mutableListOf<Int>()
+        rule.setContent {
+            state = rememberLazyGridState()
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.mainAxisSize(10.dp),
+                state = state
+            ) {
+                items(count) { index ->
+                    composedIndexes.add(index)
+                    Box(Modifier.size(20.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            composedIndexes.clear()
+            count = 10
+            runBlocking(AutoTestFrameClock()) {
+                // we try to scroll to the index after 10, but we expect that the component will
+                // already be aware there is a new count and not compose items with index > 10
+                state.scrollToItem(50)
+            }
+            composedIndexes.forEach {
+                Truth.assertThat(it).isLessThan(count)
+            }
+            Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+        }
+    }
+
+    @Test
+    fun maxIntElements() {
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.size(itemSize * 2).testTag(LazyGridTag),
+                state = TvLazyGridState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
+            ) {
+                items(Int.MAX_VALUE) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("${Int.MAX_VALUE - 3}")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE - 2}")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE - 1}")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
+        rule.onNodeWithTag("0").assertDoesNotExist()
+    }
+
+    @Test
+    fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = true
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = false
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(2)
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+        val itemSizePx = 30f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.size(itemSize * 3),
+                state = rememberLazyGridState().also { state = it },
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx)
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyGridTag)
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy))
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex))
+            // but we still have a read only scroll range property
+            .assert(
+                keyIsDefined(
+                    if (orientation == Orientation.Vertical) {
+                        SemanticsProperties.VerticalScrollAxisRange
+                    } else {
+                        SemanticsProperties.HorizontalScrollAxisRange
+                    }
+                )
+            )
+    }
+
+    @Test
+    fun rtl() {
+        val gridCrossAxisSize = 30
+        val gridCrossAxisSizeDp = with(rule.density) { gridCrossAxisSize.toDp() }
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                LazyGrid(
+                    cells = 3,
+                    modifier = Modifier.crossAxisSize(gridCrossAxisSizeDp)
+                ) {
+                    items(3) {
+                        Box(Modifier.mainAxisSize(1.dp).testTag("$it"))
+                    }
+                }
+            }
+        }
+
+        val tags = if (orientation == Orientation.Vertical) {
+            listOf("0", "1", "2")
+        } else {
+            listOf("2", "1", "0")
+        }
+        rule.onNodeWithTag(tags[0])
+            .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp * 2 / 3)
+        rule.onNodeWithTag(tags[1])
+            .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp / 3)
+        rule.onNodeWithTag(tags[2]).assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun withMissingItems() {
+        val itemMainAxisSize = with(rule.density) { 30.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.mainAxisSize(itemMainAxisSize + 1.dp),
+                state = state
+            ) {
+                items((0..8).map { it.toString() }) {
+                    if (it != "3") {
+                        Box(Modifier.mainAxisSize(itemMainAxisSize).testTag(it))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        rule.onNodeWithTag("1").assertIsDisplayed()
+        rule.onNodeWithTag("2").assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+        rule.onNodeWithTag("1").assertIsNotDisplayed()
+        rule.onNodeWithTag("2").assertIsDisplayed()
+        rule.onNodeWithTag("4").assertIsDisplayed()
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        rule.onNodeWithTag("6").assertDoesNotExist()
+        rule.onNodeWithTag("7").assertDoesNotExist()
+    }
+
+    @Test
+    fun passingNegativeItemsCountIsNotAllowed() {
+        var exception: Exception? = null
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(cells = 1) {
+                try {
+                    items(-1) {
+                        Box(Modifier)
+                    }
+                } catch (e: Exception) {
+                    exception = e
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
+        }
+    }
+
+    @Test
+    fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+        var remeasureCount = 0
+        val layoutModifier = Modifier.layout { measurable, constraints ->
+            remeasureCount++
+            val placeable = measurable.measure(constraints)
+            layout(placeable.width, placeable.height) {
+                placeable.place(0, 0)
+            }
+        }
+        val counter = mutableStateOf(0)
+
+        rule.setContentWithTestViewConfiguration {
+            counter.value // just to trigger recomposition
+            LazyGrid(
+                cells = 1,
+                // this will return a new object everytime causing LazyGrid recomposition
+                // without causing remeasure
+                modifier = Modifier.composed { layoutModifier }
+            ) {
+                items(1) {
+                    Spacer(Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(remeasureCount).isEqualTo(1)
+            counter.value++
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(remeasureCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+        var recomposeCount = 0
+        lateinit var state: TvLazyGridState
+
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyGridState()
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.composed {
+                    recomposeCount++
+                    Modifier
+                }.size(100.dp),
+                state
+            ) {
+                items(1000) {
+                    Spacer(Modifier.size(100.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(recomposeCount).isEqualTo(1)
+
+            runBlocking {
+                state.scrollToItem(100)
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(recomposeCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun zIndexOnItemAffectsDrawingOrder() {
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.size(6.dp).testTag(LazyGridTag)
+            ) {
+                items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
+                    Box(
+                        Modifier
+                            .axisSize(6.dp, 2.dp)
+                            .zIndex(if (color == Color.Green) 1f else 0f)
+                            .drawBehind {
+                                drawRect(
+                                    color,
+                                    topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
+                                    size = Size(20.dp.toPx(), 20.dp.toPx())
+                                )
+                            })
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyGridTag)
+            .captureToImage()
+            .assertPixels { Color.Green }
+    }
+
+    @Test
+    fun customGridCells() {
+        val items = (1..5).map { it.toString() }
+
+        rule.setContent {
+            LazyGrid(
+                // Two columns in ratio 1:2
+                cells = object : TvGridCells {
+                    override fun Density.calculateCrossAxisCellSizes(
+                        availableSize: Int,
+                        spacing: Int
+                    ): List<Int> {
+                        val availableCrossAxis = availableSize - spacing
+                        val columnSize = availableCrossAxis / 3
+                        return listOf(columnSize, columnSize * 2)
+                    }
+                },
+                modifier = Modifier.axisSize(300.dp, 100.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(100.dp)
+
+        rule.onNodeWithTag("2")
+            .assertCrossAxisStartPositionInRootIsEqualTo(100.dp)
+            .assertCrossAxisSizeIsEqualTo(200.dp)
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("5")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun onlyOneInitialMeasurePass() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            LazyGrid(
+                1,
+                Modifier.requiredSize(100.dp).testTag(LazyGridTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.numMeasurePasses).isEqualTo(1)
+        }
+    }
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+    isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun ComposeContentTestRule.keyPress(keyCode: Int, numberOfPresses: Int = 1) {
+    for (index in 0 until numberOfPresses)
+        InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
new file mode 100644
index 0000000..0f06a2b
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -0,0 +1,1206 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridsContentPaddingTest {
+    private val LazyListTag = "LazyList"
+    private val ItemTag = "item"
+    private val ContainerTag = "container"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+    private var smallPaddingSize: Dp = Dp.Infinity
+    private var itemSizePx = 50f
+    private var smallPaddingSizePx = 12f
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = itemSizePx.toDp()
+            smallPaddingSize = smallPaddingSizePx.toDp()
+        }
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingIsApplied() {
+        lateinit var state: TvLazyGridState
+        val containerSize = itemSize * 2
+        val largePaddingSize = itemSize
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(containerSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    start = smallPaddingSize,
+                    top = largePaddingSize,
+                    end = smallPaddingSize,
+                    bottom = largePaddingSize
+                )
+            ) {
+                items(listOf(1)) {
+                    Spacer(Modifier.height(itemSize).testTag(ItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+            .assertTopPositionInRootIsEqualTo(largePaddingSize)
+            .assertWidthIsEqualTo(containerSize - smallPaddingSize * 2)
+            .assertHeightIsEqualTo(itemSize)
+
+        state.scrollBy(largePaddingSize)
+
+        rule.onNodeWithTag(ItemTag)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingIsNotAffectingScrollPosition() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(itemSize * 2)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    top = itemSize,
+                    bottom = itemSize
+                )
+            ) {
+                items(listOf(1)) {
+                    Spacer(Modifier.height(itemSize).testTag(ItemTag))
+                }
+            }
+        }
+
+        state.assertScrollPosition(0, 0.dp)
+
+        state.scrollBy(itemSize)
+
+        state.assertScrollPosition(0, itemSize)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: TvLazyGridState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(padding)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize + padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+        state.scrollBy(padding)
+
+        state.assertScrollPosition(1, padding - itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun verticalGrid_scrollBackwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: TvLazyGridState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(itemSize + padding * 2)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize * 1.5f)
+
+        state.assertScrollPosition(1, itemSize * 0.5f)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardTillTheEnd() {
+        lateinit var state: TvLazyGridState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+        // there are no space to scroll anymore, so it should change nothing
+        state.scrollBy(10.dp)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardTillTheEndAndABitBack() {
+        lateinit var state: TvLazyGridState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            TvLazyVerticalGrid(
+                columns = TvGridCells.Fixed(1),
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyGridState().also { state = it },
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize / 2)
+
+        state.assertScrollPosition(2, itemSize / 2)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingFixedWidthContainer() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag).width(itemSize + 8.dp)) {
+                TvLazyVerticalGrid(
+                    columns = TvGridCells.Fixed(1),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                    items(listOf(1)) {
+                        Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(2.dp)
+            .assertTopPositionInRootIsEqualTo(4.dp)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+            .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingAndNoContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                TvLazyVerticalGrid(
+                    columns = TvGridCells.Fixed(1),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) { }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(8.dp)
+            .assertHeightIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingAndZeroSizedItem() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                TvLazyVerticalGrid(
+                    columns = TvGridCells.Fixed(1),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                    items(0) { }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(8.dp)
+            .assertHeightIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun verticalGrid_contentPaddingAndReverseLayout() {
+        val topPadding = itemSize * 2
+        val bottomPadding = itemSize / 2
+        val listSize = itemSize * 3
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(listSize),
+                contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
+            ) {
+                items(3) { index ->
+                    Box(Modifier.size(itemSize).testTag("$index"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+        // Partially visible.
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize / 2)
+
+        // Scroll to the top.
+        state.scrollBy(itemSize * 2.5f)
+
+        rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
+        // Shouldn't be visible
+        rule.onNodeWithTag("1").assertIsNotDisplayed()
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+    }
+
+    @Test
+    fun column_overscrollWithContentPadding() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(
+                        vertical = smallPaddingSize
+                    )
+                ) {
+                    items(2) {
+                        Box(Modifier.testTag("$it").height(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            runBlocking {
+                // itemSizePx is the maximum offset, plus if we overscroll the content padding
+                // the layout mechanism will decide the item 0 is not needed until we start
+                // filling the over scrolled gap.
+                state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_initialState() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(1, 0.dp)
+            state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+        // the whole end content padding is displayed
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 4.5f)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 1.5f)
+            state.assertVisibleItems(3 to -itemSize * 1.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_initialState() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 2)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(2, 0.dp)
+            state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+        // only the end content padding is displayed
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(
+            itemSize * 1.5f + // container size
+                itemSize * 2 + // start padding
+                itemSize * 3 // all items
+        )
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 3.5f)
+            state.assertVisibleItems(3 to -itemSize * 3.5f)
+        }
+    }
+
+    // @Test
+    // fun row_contentPaddingIsApplied() {
+    //     lateinit var state: LazyGridState
+    //     val containerSize = itemSize * 2
+    //     val largePaddingSize = itemSize
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(containerSize)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 top = smallPaddingSize,
+    //                 start = largePaddingSize,
+    //                 bottom = smallPaddingSize,
+    //                 end = largePaddingSize
+    //             )
+    //         ) {
+    //             items(listOf(1)) {
+    //                 Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ItemTag)
+    //         .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+    //         .assertLeftPositionInRootIsEqualTo(largePaddingSize)
+    //         .assertHeightIsEqualTo(containerSize - smallPaddingSize * 2)
+    //         .assertWidthIsEqualTo(itemSize)
+
+    //     state.scrollBy(largePaddingSize)
+
+    //     rule.onNodeWithTag(ItemTag)
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //         .assertWidthIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_contentPaddingIsNotAffectingScrollPosition() {
+    //     lateinit var state: LazyGridState
+    //     val itemSize = with(rule.density) {
+    //         50.dp.roundToPx().toDp()
+    //     }
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(itemSize * 2)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 start = itemSize,
+    //                 end = itemSize
+    //             )
+    //         ) {
+    //             items(listOf(1)) {
+    //                 Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
+    //             }
+    //         }
+    //     }
+
+    //     state.assertScrollPosition(0, 0.dp)
+
+    //     state.scrollBy(itemSize)
+
+    //     state.assertScrollPosition(0, itemSize)
+    // }
+
+    // @Test
+    // fun row_scrollForwardItemWithinStartPaddingDisplayed() {
+    //     lateinit var state: LazyGridState
+    //     val padding = itemSize * 1.5f
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(padding * 2 + itemSize)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 start = padding,
+    //                 end = padding
+    //             )
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(padding)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize + padding)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+    //     state.scrollBy(padding)
+
+    //     state.assertScrollPosition(1, padding - itemSize)
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 3)
+    // }
+
+    // @Test
+    // fun row_scrollBackwardItemWithinStartPaddingDisplayed() {
+    //     lateinit var state: LazyGridState
+    //     val padding = itemSize * 1.5f
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(itemSize + padding * 2)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 start = padding,
+    //                 end = padding
+    //             )
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+    //             }
+    //         }
+    //     }
+
+    //     state.scrollBy(itemSize * 3)
+    //     state.scrollBy(-itemSize * 1.5f)
+
+    //     state.assertScrollPosition(1, itemSize * 0.5f)
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+    // }
+
+    // @Test
+    // fun row_scrollForwardTillTheEnd() {
+    //     lateinit var state: LazyGridState
+    //     val padding = itemSize * 1.5f
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(padding * 2 + itemSize)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 start = padding,
+    //                 end = padding
+    //             )
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+    //             }
+    //         }
+    //     }
+
+    //     state.scrollBy(itemSize * 3)
+
+    //     state.assertScrollPosition(3, 0.dp)
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+    //     // there are no space to scroll anymore, so it should change nothing
+    //     state.scrollBy(10.dp)
+
+    //     state.assertScrollPosition(3, 0.dp)
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+    // }
+
+    // @Test
+    // fun row_scrollForwardTillTheEndAndABitBack() {
+    //     lateinit var state: LazyGridState
+    //     val padding = itemSize * 1.5f
+    //     rule.setContent {
+    //         LazyRow(
+    //             modifier = Modifier.requiredSize(padding * 2 + itemSize)
+    //                 .testTag(LazyListTag),
+    //             state = rememberLazyGridState().also { state = it },
+    //             contentPadding = PaddingValues(
+    //                 start = padding,
+    //                 end = padding
+    //             )
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+    //             }
+    //         }
+    //     }
+
+    //     state.scrollBy(itemSize * 3)
+    //     state.scrollBy(-itemSize / 2)
+
+    //     state.assertScrollPosition(2, itemSize / 2)
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    // }
+
+    // @Test
+    // fun row_contentPaddingAndWrapContent() {
+    //     rule.setContent {
+    //         Box(modifier = Modifier.testTag(ContainerTag)) {
+    //             LazyRow(
+    //                 contentPadding = PaddingValues(
+    //                     start = 2.dp,
+    //                     top = 4.dp,
+    //                     end = 6.dp,
+    //                     bottom = 8.dp
+    //                 )
+    //             ) {
+    //                 items(listOf(1)) {
+    //                     Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ItemTag)
+    //         .assertLeftPositionInRootIsEqualTo(2.dp)
+    //         .assertTopPositionInRootIsEqualTo(4.dp)
+    //         .assertWidthIsEqualTo(itemSize)
+    //         .assertHeightIsEqualTo(itemSize)
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //         .assertTopPositionInRootIsEqualTo(0.dp)
+    //         .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+    //         .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+    // }
+
+    // @Test
+    // fun row_contentPaddingAndNoContent() {
+    //     rule.setContent {
+    //         Box(modifier = Modifier.testTag(ContainerTag)) {
+    //             LazyRow(
+    //                 contentPadding = PaddingValues(
+    //                     start = 2.dp,
+    //                     top = 4.dp,
+    //                     end = 6.dp,
+    //                     bottom = 8.dp
+    //                 )
+    //             ) { }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //         .assertTopPositionInRootIsEqualTo(0.dp)
+    //         .assertWidthIsEqualTo(8.dp)
+    //         .assertHeightIsEqualTo(12.dp)
+    // }
+
+    // @Test
+    // fun row_contentPaddingAndZeroSizedItem() {
+    //     rule.setContent {
+    //         Box(modifier = Modifier.testTag(ContainerTag)) {
+    //             LazyRow(
+    //                 contentPadding = PaddingValues(
+    //                     start = 2.dp,
+    //                     top = 4.dp,
+    //                     end = 6.dp,
+    //                     bottom = 8.dp
+    //                 )
+    //             ) {
+    //                 items(0) {}
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //         .assertTopPositionInRootIsEqualTo(0.dp)
+    //         .assertWidthIsEqualTo(8.dp)
+    //         .assertHeightIsEqualTo(12.dp)
+    // }
+
+    // @Test
+    // fun row_contentPaddingAndReverseLayout() {
+    //     val startPadding = itemSize * 2
+    //     val endPadding = itemSize / 2
+    //     val listSize = itemSize * 3
+    //     lateinit var state: LazyGridState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyGridState().also { state = it },
+    //             modifier = Modifier.requiredSize(listSize),
+    //             contentPadding = PaddingValues(start = startPadding, end = endPadding),
+    //         ) {
+    //             items(3) { index ->
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$index"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize * 2)
+    //     // Partially visible.
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(-itemSize / 2)
+
+    //     // Scroll to the top.
+    //     state.scrollBy(itemSize * 2.5f)
+
+    //     rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(startPadding)
+    //     // Shouldn't be visible
+    //     rule.onNodeWithTag("1").assertIsNotDisplayed()
+    //     rule.onNodeWithTag("0").assertIsNotDisplayed()
+    // }
+
+    // @Test
+    // fun row_overscrollWithContentPadding() {
+    //     lateinit var state: LazyListState
+    //     rule.setContent {
+    //         state = rememberLazyListState()
+    //         Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+    //             LazyRow(
+    //                 state = state,
+    //                 contentPadding = PaddingValues(
+    //                     horizontal = smallPaddingSize
+    //                 )
+    //             ) {
+    //                 items(2) {
+    //                     Box(Modifier.testTag("$it").fillParentMaxSize())
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+    //         .assertWidthIsEqualTo(itemSize)
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+    //         .assertWidthIsEqualTo(itemSize)
+
+    //     rule.runOnIdle {
+    //         runBlocking {
+    //             // itemSizePx is the maximum offset, plus if we overscroll the content padding
+    //             // the layout mechanism will decide the item 0 is not needed until we start
+    //             // filling the over scrolled gap.
+    //             state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+    //         .assertWidthIsEqualTo(itemSize)
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+    //         .assertWidthIsEqualTo(itemSize)
+    // }
+
+    private fun TvLazyGridState.scrollBy(offset: Dp) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+        }
+    }
+
+    private fun TvLazyGridState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+        assertThat(this@assertScrollPosition.firstVisibleItemIndex).isEqualTo(index)
+        assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+    }
+
+    private fun TvLazyGridState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+        assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+            .isEqualTo(from.roundToPx() to to.roundToPx())
+    }
+
+    private fun TvLazyGridState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+        with(rule.density) {
+            assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset.y })
+                .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+        }
+
+    fun TvLazyGridState.scrollTo(index: Int) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            scrollToItem(index)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
new file mode 100644
index 0000000..e99a386
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+import org.junit.Test
+
+class LazyGridsIndexedTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun lazyVerticalGridShowsIndexedItems() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.height(101.dp).testTag("$index-$item")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun verticalGridWithIndexesComposedWithCorrectIndexAndItem() {
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    BasicText(
+                        "${index}x$item", Modifier.requiredHeight(100.dp)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithText("0x0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertTopPositionInRootIsEqualTo(100.dp)
+    }
+
+    // @Test
+    // fun lazyRowShowsIndexedItems() {
+    //     val items = (1..4).map { it.toString() }
+
+    //     rule.setContent {
+    //         LazyRow(Modifier.width(200.dp)) {
+    //             itemsIndexed(items) { index, item ->
+    //                 Spacer(
+    //                     Modifier.width(101.dp).fillParentMaxHeight()
+    //                         .testTag("$index-$item")
+    //                 )
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("0-1")
+    //         .assertIsDisplayed()
+
+    //     rule.onNodeWithTag("1-2")
+    //         .assertIsDisplayed()
+
+    //     rule.onNodeWithTag("2-3")
+    //         .assertDoesNotExist()
+
+    //     rule.onNodeWithTag("3-4")
+    //         .assertDoesNotExist()
+    // }
+
+    // @Test
+    // fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+    //     val items = (0..1).map { it.toString() }
+
+    //     rule.setContent {
+    //         LazyRow(Modifier.width(200.dp)) {
+    //             itemsIndexed(items) { index, item ->
+    //                 BasicText(
+    //                     "${index}x$item", Modifier.fillParentMaxHeight().requiredWidth(100.dp)
+    //                 )
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithText("0x0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+
+    //     rule.onNodeWithText("1x1")
+    //         .assertLeftPositionInRootIsEqualTo(100.dp)
+    // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
new file mode 100644
index 0000000..c3cd0a9
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyGridsReverseLayoutTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+    }
+
+    @Test
+    fun verticalGrid_reverseLayout() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = true
+            ) {
+                items(4) {
+                    Box(Modifier.height(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_emitTwoElementsAsOneItem() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = true
+            ) {
+                items(4) {
+                    Box(Modifier.height(itemSize).testTag((it * 2).toString()))
+                    Box(Modifier.height(itemSize).testTag((it * 2 + 1).toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("4")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("5")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("6")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("7")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun verticalGrid_initialScrollPositionIs0() {
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(2),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun verticalGrid_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardHalfWay() {
+        lateinit var state: TvLazyGridState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    // @Test
+    // fun row_emitTwoElementsAsOneItem_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_emitTwoItems_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //             }
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_initialScrollPositionIs0() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //     }
+    // }
+
+    // @Test
+    // fun row_scrollInWrongDirectionDoesNothing() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     // we scroll down and as the scrolling is reversed it shouldn't affect anything
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = itemSize, density = rule.density)
+
+    //     rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_scrollForwardHalfWay() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+    //     val scrolled = rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //         with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+    //     }
+
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(scrolled)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+    // }
+
+    // @Test
+    // fun row_scrollForwardTillTheEnd() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     // we scroll a bit more than it is possible just to make sure we would stop correctly
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+    //     rule.runOnIdle {
+    //         with(rule.density) {
+    //             val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+    //                 itemSize * state.firstVisibleItemIndex
+    //             assertThat(realOffset).isEqualTo(itemSize * 2)
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true
+    //             ) {
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                     Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    // }
+
+    // @Test
+    // fun row_rtl_emitTwoItems_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true
+    //             ) {
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 }
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    // }
+
+    // @Test
+    // fun row_rtl_scrollForwardHalfWay() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true,
+    //                 state = rememberLazyListState().also { state = it },
+    //                 modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //             ) {
+    //                 items((0..2).toList()) {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+    //     val scrolled = rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //         with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(-scrolled)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+    // }
+
+    @Test
+    fun verticalGrid_whenParameterChanges() {
+        var reverse by mutableStateOf(true)
+        rule.setContentWithTestViewConfiguration {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = reverse
+            ) {
+                items(4) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            reverse = false
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    // @Test
+    // fun row_whenParameterChanges() {
+    //     var reverse by mutableStateOf(true)
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = reverse
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+
+    //     rule.runOnIdle {
+    //         reverse = false
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..f4e519e
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun visibleItemsStateRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        restorationTester.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+                item {
+                    realState[0] = rememberSaveable { counter0++ }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+                items((1..2).toList()) {
+                    if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: TvLazyGridState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyGridState().also { state = it }
+            ) {
+                items((0..10).toList()) {
+                    if (it == 0) {
+                        realState = rememberSaveable { counter0++ }
+                        DisposableEffect(Unit) {
+                            onDispose {
+                                itemDisposed = true
+                            }
+                        }
+                    }
+                    Box(Modifier.requiredSize(30.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                // we scroll through multiple items to make sure the 0th element is not kept in
+                // the reusable items buffer
+                state.scrollToItem(3)
+                state.scrollToItem(5)
+                state.scrollToItem(8)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        lateinit var state: TvLazyGridState
+        var realState = arrayOf(0, 0)
+        restorationTester.setContent {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyGridState().also { state = it }
+            ) {
+                items((0..1).toList()) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else {
+                        realState[1] = rememberSaveable { counter1++ }
+                    }
+                    Box(Modifier.requiredSize(30.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            runBlocking {
+                state.scrollToItem(1, 5)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            realState = arrayOf(0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: TvLazyGridState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            TvLazyVerticalGrid(
+                TvGridCells.Fixed(1),
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyGridState().also { state = it }
+            ) {
+                items((0..10).toList()) {
+                    if (it == 0) {
+                        TvLazyRow {
+                            item {
+                                realState = rememberSaveable { counter0++ }
+                                DisposableEffect(Unit) {
+                                    onDispose {
+                                        itemDisposed = true
+                                    }
+                                }
+                                Box(Modifier.requiredSize(30.dp))
+                            }
+                        }
+                    } else {
+                        Box(Modifier.requiredSize(30.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                // we scroll through multiple items to make sure the 0th element is not kept in
+                // the reusable items buffer
+                state.scrollToItem(3)
+                state.scrollToItem(5)
+                state.scrollToItem(8)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun stateRestoredWhenUsedWithCustomKeys() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        restorationTester.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+                items(3, key = { "$it" }) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun stateRestoredWhenUsedWithCustomKeysAfterReordering() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        var list by mutableStateOf(listOf(0, 1, 2))
+        restorationTester.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+                items(list, key = { "$it" }) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2)
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(0)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
new file mode 100644
index 0000000..404218a
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.tv.foundation.lazy.list.TestTouchSlop
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyNestedScrollingTest {
+    private val LazyTag = "LazyTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val expectedDragOffset = 20f
+    private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
+
+    @Test
+    fun verticalGrid_nestedScrollingBackwardInitially() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    Modifier.requiredSize(100.dp).testTag(LazyTag)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(100f)
+        }
+    }
+
+    @Test
+    fun verticalGrid_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    Modifier.requiredSize(100.dp).testTag(LazyTag),
+                ) {
+                    items(items) {
+                        Box(Modifier.requiredHeight(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll forward
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+        // scroll back so we again on 0 position
+        // we scroll one extra dp to prevent rounding issues
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 100f, y = 100f))
+                moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun verticalGrid_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+        val items = (1..2).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    Modifier.requiredSize(100.dp).testTag(LazyTag)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(40.dp).testTag("$it"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun verticalGrid_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyVerticalGrid(
+                    TvGridCells.Fixed(1),
+                    Modifier.requiredSize(100.dp).testTag(LazyTag)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll till the end
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+
+    // @Test
+    // fun row_nestedScrollingBackwardInitially() = runBlocking {
+    //     val items = (1..3).toList()
+    //     var draggedOffset = 0f
+    //     val scrollable = ScrollableState {
+    //         draggedOffset += it
+    //         it
+    //     }
+    //     rule.setContentWithTestViewConfiguration {
+    //         Box(
+    //             Modifier.scrollable(
+    //                 orientation = Orientation.Horizontal,
+    //                 state = scrollable
+    //             )
+    //         ) {
+    //             LazyRow(
+    //                 modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+    //             ) {
+    //                 items(items) {
+    //                     Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(LazyTag)
+    //         .performTouchInput {
+    //             down(Offset(x = 10f, y = 10f))
+    //             moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+    //             up()
+    //         }
+
+    //     rule.runOnIdle {
+    //         Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+    //     }
+    // }
+
+    // @Test
+    // fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+    //     val items = (1..3).toList()
+    //     var draggedOffset = 0f
+    //     val scrollable = ScrollableState {
+    //         draggedOffset += it
+    //         it
+    //     }
+    //     rule.setContentWithTestViewConfiguration {
+    //         Box(
+    //             Modifier.scrollable(
+    //                 orientation = Orientation.Horizontal,
+    //                 state = scrollable
+    //             )
+    //         ) {
+    //             LazyRow(
+    //                 modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+    //             ) {
+    //                 items(items) {
+    //                     Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     // scroll forward
+    //     rule.onNodeWithTag(LazyTag)
+    //         .scrollBy(x = 20.dp, density = rule.density)
+
+    //     // scroll back so we again on 0 position
+    //     // we scroll one extra dp to prevent rounding issues
+    //     rule.onNodeWithTag(LazyTag)
+    //         .scrollBy(x = -(21.dp), density = rule.density)
+
+    //     rule.onNodeWithTag(LazyTag)
+    //         .performTouchInput {
+    //             draggedOffset = 0f
+    //             down(Offset(x = 10f, y = 10f))
+    //             moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+    //             up()
+    //         }
+
+    //     rule.runOnIdle {
+    //         Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+    //     }
+    // }
+
+    // @Test
+    // fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+    //     val items = (1..2).toList()
+    //     var draggedOffset = 0f
+    //     val scrollable = ScrollableState {
+    //         draggedOffset += it
+    //         it
+    //     }
+    //     rule.setContentWithTestViewConfiguration {
+    //         Box(
+    //             Modifier.scrollable(
+    //                 orientation = Orientation.Horizontal,
+    //                 state = scrollable
+    //             )
+    //         ) {
+    //             LazyRow(
+    //                 modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+    //             ) {
+    //                 items(items) {
+    //                     Spacer(Modifier.requiredSize(40.dp).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(LazyTag)
+    //         .performTouchInput {
+    //             down(Offset(x = 10f, y = 10f))
+    //             moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+    //             up()
+    //         }
+
+    //     rule.runOnIdle {
+    //         Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+    //     }
+    // }
+
+    // @Test
+    // fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+    //     val items = (1..3).toList()
+    //     var draggedOffset = 0f
+    //     val scrollable = ScrollableState {
+    //         draggedOffset += it
+    //         it
+    //     }
+    //     rule.setContentWithTestViewConfiguration {
+    //         Box(
+    //             Modifier.scrollable(
+    //                 orientation = Orientation.Horizontal,
+    //                 state = scrollable
+    //             )
+    //         ) {
+    //             LazyRow(
+    //                 modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+    //             ) {
+    //                 items(items) {
+    //                     Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     // scroll till the end
+    //     rule.onNodeWithTag(LazyTag)
+    //         .scrollBy(x = 55.dp, density = rule.density)
+
+    //     rule.onNodeWithTag(LazyTag)
+    //         .performTouchInput {
+    //             draggedOffset = 0f
+    //             down(Offset(x = 10f, y = 10f))
+    //             moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+    //             up()
+    //         }
+
+    //     rule.runOnIdle {
+    //         Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+    //     }
+    // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
new file mode 100644
index 0000000..8af2b8d
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import android.R.id.accessibilityActionScrollDown
+import android.R.id.accessibilityActionScrollLeft
+import android.R.id.accessibilityActionScrollRight
+import android.R.id.accessibilityActionScrollUp
+import android.view.View
+import android.view.accessibility.AccessibilityNodeProvider
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
+import androidx.test.filters.MediumTest
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollAccessibilityTest(
+    private val config: TestConfig
+) : BaseLazyGridTestWithOrientation(config.orientation) {
+
+    data class TestConfig(
+        val orientation: Orientation,
+        val rtl: Boolean,
+        val reversed: Boolean
+    ) {
+        val horizontal = orientation == Orientation.Horizontal
+        val vertical = !horizontal
+
+        override fun toString(): String {
+            return (if (orientation == Orientation.Horizontal) "horizontal" else "vertical") +
+                (if (rtl) ",rtl" else ",ltr") +
+                (if (reversed) ",reversed" else "")
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() =
+            listOf(Orientation.Horizontal, Orientation.Vertical).flatMap { horizontal ->
+                listOf(false, true).flatMap { rtl ->
+                    listOf(false, true).map { reversed ->
+                        TestConfig(horizontal, rtl, reversed)
+                    }
+                }
+            }
+    }
+
+    private val scrollerTag = "ScrollerTest"
+    private var composeView: View? = null
+    private val accessibilityNodeProvider: AccessibilityNodeProvider
+        get() = checkNotNull(composeView) {
+            "composeView not initialized. Did `composeView = LocalView.current` not work?"
+        }.let { composeView ->
+            ViewCompat
+                .getAccessibilityDelegate(composeView)!!
+                .getAccessibilityNodeProvider(composeView)!!
+                .provider as AccessibilityNodeProvider
+        }
+
+    @Test
+    fun scrollForward() {
+        testRelativeDirection(58, ACTION_SCROLL_FORWARD)
+    }
+
+    @Test
+    fun scrollBackward() {
+        testRelativeDirection(41, ACTION_SCROLL_BACKWARD)
+    }
+
+    @Test
+    fun scrollRight() {
+        testAbsoluteDirection(58, accessibilityActionScrollRight, config.horizontal)
+    }
+
+    @Test
+    fun scrollLeft() {
+        testAbsoluteDirection(41, accessibilityActionScrollLeft, config.horizontal)
+    }
+
+    @Test
+    fun scrollDown() {
+        testAbsoluteDirection(58, accessibilityActionScrollDown, config.vertical)
+    }
+
+    @Test
+    fun scrollUp() {
+        testAbsoluteDirection(41, accessibilityActionScrollUp, config.vertical)
+    }
+
+    @Test
+    fun verifyScrollActionsAtStart() {
+        createScrollableContent_StartAtStart()
+        verifyNodeInfoScrollActions(
+            expectForward = !config.reversed,
+            expectBackward = config.reversed
+        )
+    }
+
+    @Test
+    fun verifyScrollActionsInMiddle() {
+        createScrollableContent_StartInMiddle()
+        verifyNodeInfoScrollActions(
+            expectForward = true,
+            expectBackward = true
+        )
+    }
+
+    @Test
+    fun verifyScrollActionsAtEnd() {
+        createScrollableContent_StartAtEnd()
+        verifyNodeInfoScrollActions(
+            expectForward = config.reversed,
+            expectBackward = !config.reversed
+        )
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+     * has been reached. The canonical target is the item that we expect to see when moving
+     * forward in a non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR).
+     * The actual target is either the canonical target or the target that is as far from the
+     * middle of the lazy list as the canonical target, but on the other side of the middle,
+     * depending on the [configuration][config].
+     */
+    private fun testRelativeDirection(canonicalTarget: Int, accessibilityAction: Int) {
+        val target = if (!config.reversed) canonicalTarget else 100 - canonicalTarget - 1
+        testScrollAction(target, accessibilityAction)
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+     * has been reached (but only if we [expect][expectActionSuccess] the action to succeed).
+     * The canonical target is the item that we expect to see when moving forward in a
+     * non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual
+     * target is either the canonical target or the target that is as far from the middle of the
+     * scrollable as the canonical target, but on the other side of the middle, depending on the
+     * [configuration][config].
+     */
+    private fun testAbsoluteDirection(
+        canonicalTarget: Int,
+        accessibilityAction: Int,
+        expectActionSuccess: Boolean
+    ) {
+        var target = canonicalTarget
+        if (config.horizontal && config.rtl) {
+            target = 100 - target - 1
+        }
+        if (config.reversed) {
+            target = 100 - target - 1
+        }
+        testScrollAction(target, accessibilityAction, expectActionSuccess)
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [target] has been
+     * reached (but only if we [expect][expectActionSuccess] the action to succeed).
+     */
+    private fun testScrollAction(
+        target: Int,
+        accessibilityAction: Int,
+        expectActionSuccess: Boolean = true
+    ) {
+        createScrollableContent_StartInMiddle()
+        rule.onNodeWithText("$target").assertDoesNotExist()
+
+        val returnValue = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+            accessibilityNodeProvider.performAction(id, accessibilityAction, null)
+        }
+
+        assertThat(returnValue).isEqualTo(expectActionSuccess)
+        if (expectActionSuccess) {
+            rule.onNodeWithText("$target").assertIsDisplayed()
+        } else {
+            rule.onNodeWithText("$target").assertDoesNotExist()
+        }
+    }
+
+    /**
+     * Checks if all of the scroll actions are present or not according to what we expect based on
+     * [expectForward] and [expectBackward]. The scroll actions that are checked are forward,
+     * backward, left, right, up and down. The expectation parameters must already account for
+     * [reversing][TestConfig.reversed].
+     */
+    private fun verifyNodeInfoScrollActions(expectForward: Boolean, expectBackward: Boolean) {
+        val nodeInfo = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+            rule.runOnUiThread {
+                accessibilityNodeProvider.createAccessibilityNodeInfo(id)
+            }
+        }
+
+        val actions = nodeInfo.actionList.map { it.id }
+
+        assertThat(actions).contains(expectForward, ACTION_SCROLL_FORWARD)
+        assertThat(actions).contains(expectBackward, ACTION_SCROLL_BACKWARD)
+
+        if (config.horizontal) {
+            val expectLeft = if (config.rtl) expectForward else expectBackward
+            val expectRight = if (config.rtl) expectBackward else expectForward
+            assertThat(actions).contains(expectLeft, accessibilityActionScrollLeft)
+            assertThat(actions).contains(expectRight, accessibilityActionScrollRight)
+            assertThat(actions).contains(false, accessibilityActionScrollDown)
+            assertThat(actions).contains(false, accessibilityActionScrollUp)
+        } else {
+            assertThat(actions).contains(false, accessibilityActionScrollLeft)
+            assertThat(actions).contains(false, accessibilityActionScrollRight)
+            assertThat(actions).contains(expectForward, accessibilityActionScrollDown)
+            assertThat(actions).contains(expectBackward, accessibilityActionScrollUp)
+        }
+    }
+
+    private fun IterableSubject.contains(expectPresent: Boolean, element: Any) {
+        if (expectPresent) {
+            contains(element)
+        } else {
+            doesNotContain(element)
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts at the first item, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartAtStart() {
+        createScrollableContent {
+            // Start at the start:
+            // -> pretty basic
+            rememberLazyGridState(0, 0)
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts in the middle, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartInMiddle() {
+        createScrollableContent {
+            // Start at the middle:
+            // Content size: 100 items * 21dp per item = 2100dp
+            // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+            // Content outside viewport: 2100dp - 100dp = 2000dp
+            // -> centered when 1000dp on either side, which is 47 items + 13dp
+            rememberLazyGridState(
+                47,
+                with(LocalDensity.current) { 13.dp.roundToPx() }
+            )
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts at the last item, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartAtEnd() {
+        createScrollableContent {
+            // Start at the end:
+            // Content size: 100 items * 21dp per item = 2100dp
+            // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+            // Content outside viewport: 2100dp - 100dp = 2000dp
+            // -> at the end when offset at 2000dp, which is 95 items + 5dp
+            rememberLazyGridState(
+                95,
+                with(LocalDensity.current) { 5.dp.roundToPx() }
+            )
+        }
+    }
+
+    /**
+     * Creates a grid with a viewport of 100.dp, containing 100 items each 17.dp in size.
+     * The items have a text with their index (ASC), and where the viewport starts is determined
+     * by the given [lambda][rememberTvLazyGridState]. All properties from [config] are applied.
+     * The viewport has padding around it to make sure scroll distance doesn't include padding.
+     */
+    private fun createScrollableContent(
+        rememberTvLazyGridState: @Composable () -> TvLazyGridState
+    ) {
+        rule.setContent {
+            composeView = LocalView.current
+
+            val state = rememberTvLazyGridState()
+
+            Box(Modifier.requiredSize(200.dp).background(Color.White)) {
+                val direction = if (config.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
+                CompositionLocalProvider(LocalLayoutDirection provides direction) {
+                    LazyGrid(
+                        cells = 1,
+                        modifier = Modifier.testTag(scrollerTag).matchParentSize(),
+                        state = state,
+                        contentPadding = PaddingValues(50.dp),
+                        reverseLayout = config.reversed
+                    ) {
+                        items(100) {
+                            Box(Modifier.requiredSize(21.dp).background(Color.Yellow)) {
+                                BasicText("$it", Modifier.align(Alignment.Center))
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun <T> SemanticsNodeInteraction.withSemanticsNode(block: SemanticsNode.() -> T): T {
+        return block.invoke(fetchSemanticsNode())
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt
new file mode 100644
index 0000000..dc794c5
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.FloatSpringSpec
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+// @RunWith(Parameterized::class)
+class LazyScrollTest { // (private val orientation: Orientation)
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val vertical: Boolean
+        get() = true // orientation == Orientation.Vertical
+
+    private val itemsCount = 40
+    private lateinit var state: TvLazyGridState
+
+    private val itemSizePx = 100
+    private var itemSizeDp = Dp.Unspecified
+    private var containerSizeDp = Dp.Unspecified
+
+    lateinit var scope: CoroutineScope
+
+    @Before
+    fun setup() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            containerSizeDp = itemSizeDp * 3
+        }
+        rule.setContent {
+            state = rememberLazyGridState()
+            scope = rememberCoroutineScope()
+            TestContent()
+        }
+    }
+
+    @Test
+    fun setupWorks() {
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    }
+
+    @Test
+    fun scrollToItem() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(2)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(0)
+            state.scrollToItem(3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun scrollToItemWithOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(6, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun scrollToItemWithNegativeOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(6, -10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+        val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+        assertThat(item6Offset).isEqualTo(10)
+    }
+
+    @Test
+    fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount - 6, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+    }
+
+    @Test
+    fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(1, -(itemSizePx + 10))
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+    }
+
+    @Test
+    fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount + 4)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+    }
+
+    @Test
+    fun animateScrollBy() = runBlocking {
+        val scrollDistance = 320
+
+        val expectedLine = scrollDistance / itemSizePx // resolves to 3
+        val expectedItem = expectedLine * 2 // resolves to 6
+        val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
+
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollBy(scrollDistance.toFloat())
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(expectedItem)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+    }
+
+    @Test
+    fun animateScrollToItem() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(10, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(6, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithNegativeOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(6, -10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+        val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+        assertThat(item6Offset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(itemsCount - 6, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+    }
+
+    @Test
+    fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(2, -(itemSizePx + 10))
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+    }
+
+    @Test
+    fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(itemsCount + 2)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+    }
+
+    @Test
+    fun animatePerFrameForwardToVisibleItem() {
+        assertSpringAnimation(toIndex = 4)
+    }
+
+    @Test
+    fun animatePerFrameForwardToVisibleItemWithOffset() {
+        assertSpringAnimation(toIndex = 4, toOffset = 35)
+    }
+
+    @Test
+    fun animatePerFrameForwardToNotVisibleItem() {
+        assertSpringAnimation(toIndex = 16)
+    }
+
+    @Test
+    fun animatePerFrameForwardToNotVisibleItemWithOffset() {
+        assertSpringAnimation(toIndex = 20, toOffset = 35)
+    }
+
+    @Test
+    fun animatePerFrameBackward() {
+        assertSpringAnimation(toIndex = 2, fromIndex = 12)
+    }
+
+    @Test
+    fun animatePerFrameBackwardWithOffset() {
+        assertSpringAnimation(toIndex = 2, fromIndex = 10, fromOffset = 58)
+    }
+
+    @Test
+    fun animatePerFrameBackwardWithInitialOffset() {
+        assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
+    }
+
+    private fun assertSpringAnimation(
+        toIndex: Int,
+        toOffset: Int = 0,
+        fromIndex: Int = 0,
+        fromOffset: Int = 0
+    ) {
+        if (fromIndex != 0 || fromOffset != 0) {
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollToItem(fromIndex, fromOffset)
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
+
+        rule.mainClock.autoAdvance = false
+
+        scope.launch {
+            state.animateScrollToItem(toIndex, toOffset)
+        }
+
+        while (!state.isScrollInProgress) {
+            Thread.sleep(5)
+        }
+
+        val startOffset = (fromIndex / 2 * itemSizePx + fromOffset).toFloat()
+        val endOffset = (toIndex / 2 * itemSizePx + toOffset).toFloat()
+        val spec = FloatSpringSpec()
+
+        val duration =
+            TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
+            val expectedValue =
+                spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
+            val actualValue =
+                (state.firstVisibleItemIndex / 2 * itemSizePx + state.firstVisibleItemScrollOffset)
+            assertWithMessage(
+                "On animation frame at $i index=${state.firstVisibleItemIndex} " +
+                    "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
+            ).that(actualValue).isEqualTo(expectedValue.roundToInt(), tolerance = 1)
+
+            rule.mainClock.advanceTimeBy(FrameDuration)
+            expectedTime += FrameDuration
+            assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            rule.waitForIdle()
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
+    }
+
+    @Composable
+    private fun TestContent() {
+        if (vertical) {
+            TvLazyVerticalGrid(TvGridCells.Fixed(2), Modifier.height(containerSizeDp), state) {
+                items(itemsCount) {
+                    ItemContent()
+                }
+            }
+        } else {
+            // LazyRow(Modifier.width(300.dp), state) {
+            //     items(items) {
+            //         ItemContent()
+            //     }
+            // }
+        }
+    }
+
+    @Composable
+    private fun ItemContent() {
+        val modifier = if (vertical) {
+            Modifier.height(itemSizeDp)
+        } else {
+            Modifier.width(itemSizeDp)
+        }
+        Spacer(modifier)
+    }
+
+    // companion object {
+    //     @JvmStatic
+    //     @Parameterized.Parameters(name = "{0}")
+    //     fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+    // }
+}
+
+private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt
new file mode 100644
index 0000000..68c75ee
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
+import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests the semantics properties defined on a LazyGrid:
+ * - GetIndexForKey
+ * - ScrollToIndex
+ *
+ * GetIndexForKey:
+ * Create a lazy grid, iterate over all indices, verify key of each of them
+ *
+ * ScrollToIndex:
+ * Create a lazy grid, scroll to a line off screen, verify shown items
+ *
+ * All tests performed in [runTest], scenarios set up in the test methods.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazySemanticsTest {
+    private val N = 20
+    private val LazyGridTag = "lazy_grid"
+    private val LazyGridModifier = Modifier.testTag(LazyGridTag).requiredSize(100.dp)
+
+    private fun tag(index: Int): String = "tag_$index"
+    private fun key(index: Int): String = "key_$index"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun itemSemantics_verticalGrid() {
+        rule.setContent {
+            TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier) {
+                repeat(N) {
+                    item(key = key(it)) {
+                        SpacerInColumn(it)
+                    }
+                }
+            }
+        }
+        runTest()
+    }
+
+    @Test
+    fun itemsSemantics_verticalGrid() {
+        rule.setContent {
+            val state = rememberLazyGridState()
+            TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier, state) {
+                items(items = List(N) { it }, key = { key(it) }) {
+                    SpacerInColumn(it)
+                }
+            }
+        }
+        runTest()
+    }
+
+    // @Test
+    // fun itemSemantics_row() {
+    //     rule.setContent {
+    //         LazyRow(LazyGridModifier) {
+    //             repeat(N) {
+    //                 item(key = key(it)) {
+    //                     SpacerInRow(it)
+    //                 }
+    //             }
+    //         }
+    //     }
+    //     runTest()
+    // }
+
+    // @Test
+    // fun itemsSemantics_row() {
+    //     rule.setContent {
+    //         LazyRow(LazyGridModifier) {
+    //             items(items = List(N) { it }, key = { key(it) }) {
+    //                 SpacerInRow(it)
+    //             }
+    //         }
+    //     }
+    //     runTest()
+    // }
+
+    private fun runTest() {
+        checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
+
+        // Verify IndexForKey
+        rule.onNodeWithTag(LazyGridTag).assert(
+            SemanticsMatcher.keyIsDefined(IndexForKey).and(
+                SemanticsMatcher("keys match") { node ->
+                    val actualIndex = node.config.getOrNull(IndexForKey)!!
+                    (0 until N).all { expectedIndex ->
+                        expectedIndex == actualIndex.invoke(key(expectedIndex))
+                    }
+                }
+            )
+        )
+
+        // Verify ScrollToIndex
+        rule.onNodeWithTag(LazyGridTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
+
+        invokeScrollToIndex(targetIndex = 10)
+        checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
+
+        invokeScrollToIndex(targetIndex = N - 1)
+        checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
+    }
+
+    private fun invokeScrollToIndex(targetIndex: Int) {
+        val node = rule.onNodeWithTag(LazyGridTag)
+            .fetchSemanticsNode("Failed: invoke ScrollToIndex")
+        rule.runOnUiThread {
+            node.config[ScrollToIndex].action!!.invoke(targetIndex)
+        }
+    }
+
+    private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
+        if (firstExpectedItem > 0) {
+            rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
+        }
+        (firstExpectedItem..lastExpectedItem).forEach {
+            rule.onNodeWithTag(tag(it)).assertExists()
+        }
+        if (firstExpectedItem < N - 1) {
+            rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
+        }
+    }
+
+    @Composable
+    private fun SpacerInColumn(index: Int) {
+        Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
+    }
+
+    @Composable
+    private fun SpacerInRow(index: Int) {
+        Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
new file mode 100644
index 0000000..d2bee39
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.tv.foundation.lazy.list.LayoutInfoTestParam
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class TvLazyGridLayoutInfoTest(
+    param: LayoutInfoTestParam
+) : BaseLazyGridTestWithOrientation(param.orientation) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            LayoutInfoTestParam(Orientation.Vertical, false),
+            LayoutInfoTestParam(Orientation.Vertical, true),
+            LayoutInfoTestParam(Orientation.Horizontal, false),
+            LayoutInfoTestParam(Orientation.Horizontal, true),
+        )
+    }
+    private val isVertical = param.orientation == Orientation.Vertical
+    private val reverseLayout = param.reverseLayout
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private var gridWidthPx: Int = itemSizePx * 2
+    private var gridWidthDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            gridWidthDp = gridWidthPx.toDp()
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 8, cells = 2)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScroll() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2, 10)
+            }
+            state.layoutInfo
+                .assertVisibleItems(count = 8, startIndex = 2, startOffset = -10, cells = 2)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithSpacing() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 1,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                mainAxisSpacedBy = itemSizeDp,
+                modifier = Modifier.axisSize(itemSizeDp, itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx, cells = 1)
+        }
+    }
+
+    @Composable
+    fun ObservingFun(state: TvLazyGridState, currentInfo: StableRef<TvLazyGridLayoutInfo?>) {
+        currentInfo.value = state.layoutInfo
+    }
+    @Test
+    fun visibleItemsAreObservableWhenWeScroll() {
+        lateinit var state: TvLazyGridState
+        val currentInfo = StableRef<TvLazyGridLayoutInfo?>(null)
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.axisSize(itemSizeDp * 2f, itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.mainAxisSize(itemSizeDp))
+                }
+            }
+            ObservingFun(state, currentInfo)
+        }
+
+        rule.runOnIdle {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            runBlocking {
+                state.scrollToItem(2, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            currentInfo.value!!
+                .assertVisibleItems(count = 8, startIndex = 2, cells = 2)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: TvLazyGridState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: TvLazyGridLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.crossAxisSize(itemSizeDp),
+                reverseLayout = reverseLayout,
+                state = rememberLazyGridState().also { state = it },
+            ) {
+                item {
+                    Box(Modifier.size(size))
+                }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(
+                count = 1,
+                expectedSize = if (isVertical) {
+                    IntSize(itemSizePx, itemSizePx * 2)
+                } else {
+                    IntSize(itemSizePx * 2, itemSizePx)
+               },
+                cells = 1
+            )
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(
+                count = 1,
+                expectedSize = IntSize(itemSizePx, itemSizePx),
+                cells = 1
+            )
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                reverseLayout = reverseLayout,
+                state = rememberLazyGridState().also { state = it },
+            ) {
+                items((0 until count).toList()) {
+                    Box(Modifier.mainAxisSize(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
+                reverseLayout = reverseLayout,
+                state = rememberLazyGridState().also { state = it },
+            ) {
+                items((0..7).toList()) {
+                    Box(Modifier.mainAxisSize(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (isVertical) {
+                    IntSize(sizePx * 2, sizePx)
+                } else {
+                    IntSize(sizePx, sizePx * 2)
+                }
+            )
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp,
+                    beforeContentCrossAxis = 2.dp,
+                    afterContentCrossAxis = 2.dp
+                ),
+                reverseLayout = reverseLayout,
+                state = rememberLazyGridState().also { state = it },
+            ) {
+                items((0..7).toList()) {
+                    Box(Modifier.mainAxisSize(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (isVertical) {
+                    IntSize(sizePx * 2, sizePx)
+                } else {
+                    IntSize(sizePx, sizePx * 2)
+                }
+            )
+        }
+    }
+
+    @Test
+    fun emptyItemsInVisibleItemsInfo() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it }
+            ) {
+                item { Box(Modifier) }
+                item { }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
+            assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun emptyContent() {
+        lateinit var state: TvLazyGridState
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        rule.setContent {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp
+                )
+            ) {
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun viewportIsLargerThenTheContent() {
+        lateinit var state: TvLazyGridState
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        rule.setContent {
+            LazyGrid(
+                cells = 1,
+                modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp
+                )
+            ) {
+                item {
+                    Box(Modifier.size(sizeDp / 2))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun reverseLayoutIsCorrect() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.size(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
+        }
+    }
+
+    @Test
+    fun orientationIsCorrect() {
+        lateinit var state: TvLazyGridState
+        rule.setContent {
+            LazyGrid(
+                cells = 2,
+                state = rememberLazyGridState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+            ) {
+                items((0..11).toList()) {
+                    Box(Modifier.mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.orientation == Orientation.Vertical).isEqualTo(isVertical)
+        }
+    }
+
+    fun TvLazyGridLayoutInfo.assertVisibleItems(
+        count: Int,
+        cells: Int,
+        startIndex: Int = 0,
+        startOffset: Int = 0,
+        expectedSize: IntSize = IntSize(itemSizePx, itemSizePx),
+        spacing: Int = 0
+    ) {
+        assertThat(visibleItemsInfo.size).isEqualTo(count)
+        if (count == 0) return
+
+        assertThat(startIndex % cells).isEqualTo(0)
+        assertThat(visibleItemsInfo.size % cells).isEqualTo(0)
+
+        var currentIndex = startIndex
+        var currentOffset = startOffset
+        var currentLine = startIndex / cells
+        var currentCell = 0
+        visibleItemsInfo.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertWithMessage("Offset of item $currentIndex")
+                .that(if (isVertical) it.offset.y else it.offset.x)
+                .isEqualTo(currentOffset)
+            assertThat(it.size).isEqualTo(expectedSize)
+            assertThat(if (isVertical) it.row else it.column)
+                .isEqualTo(currentLine)
+            assertThat(if (isVertical) it.column else it.row)
+                .isEqualTo(currentCell)
+            currentIndex++
+            currentCell++
+            if (currentCell == cells) {
+                currentCell = 0
+                ++currentLine
+                currentOffset += spacing + if (isVertical) it.size.height else it.size.width
+            }
+        }
+    }
+}
+
+class LayoutInfoTestParam(
+    val orientation: Orientation,
+    val reverseLayout: Boolean
+) {
+    override fun toString(): String {
+        return "orientation=$orientation;reverseLayout=$reverseLayout"
+    }
+}
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
new file mode 100644
index 0000000..9ab4802
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.grid.keyPress
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+
+open class BaseLazyListTestWithOrientation(private val orientation: Orientation) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val vertical: Boolean
+        get() = orientation == Orientation.Vertical
+
+    fun Modifier.mainAxisSize(size: Dp) =
+        if (vertical) {
+            this.height(size)
+        } else {
+            this.width(size)
+        }
+
+    fun Modifier.crossAxisSize(size: Dp) =
+        if (vertical) {
+            this.width(size)
+        } else {
+            this.height(size)
+        }
+
+    fun Modifier.fillMaxCrossAxis() =
+        if (vertical) {
+            this.fillMaxWidth()
+        } else {
+            this.fillMaxHeight()
+        }
+
+    fun LazyItemScope.fillParentMaxCrossAxis() =
+        if (vertical) {
+            Modifier.fillParentMaxWidth()
+        } else {
+            Modifier.fillParentMaxHeight()
+        }
+
+    fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertHeightIsEqualTo(expectedSize)
+        } else {
+            assertWidthIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertWidthIsEqualTo(expectedSize)
+        } else {
+            assertHeightIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+        val position = if (vertical) {
+            getUnclippedBoundsInRoot().top
+        } else {
+            getUnclippedBoundsInRoot().left
+        }
+        position.assertIsEqualTo(expected, tolerance = 1.dp)
+    }
+
+    fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun PaddingValues(
+        mainAxis: Dp = 0.dp,
+        crossAxis: Dp = 0.dp
+    ) = PaddingValues(
+        beforeContent = mainAxis,
+        afterContent = mainAxis,
+        beforeContentCrossAxis = crossAxis,
+        afterContentCrossAxis = crossAxis
+    )
+
+    fun PaddingValues(
+        beforeContent: Dp = 0.dp,
+        afterContent: Dp = 0.dp,
+        beforeContentCrossAxis: Dp = 0.dp,
+        afterContentCrossAxis: Dp = 0.dp,
+    ) = if (vertical) {
+        PaddingValues(
+            start = beforeContentCrossAxis,
+            top = beforeContent,
+            end = afterContentCrossAxis,
+            bottom = afterContent
+        )
+    } else {
+        PaddingValues(
+            start = beforeContent,
+            top = beforeContentCrossAxis,
+            end = afterContent,
+            bottom = afterContentCrossAxis
+        )
+    }
+
+    fun TvLazyListState.scrollBy(offset: Dp) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+        }
+    }
+
+    fun TvLazyListState.scrollTo(index: Int) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            scrollToItem(index)
+        }
+    }
+
+    fun ComposeContentTestRule.keyPress(numberOfKeyPresses: Int, reverseScroll: Boolean = false) {
+        val keyCode: Int =
+            when {
+                vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_UP
+                vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_DOWN
+                !vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_LEFT
+                !vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
+                else -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
+            }
+
+        keyPress(keyCode, numberOfKeyPresses)
+    }
+
+    @Composable
+    fun LazyColumnOrRow(
+        modifier: Modifier = Modifier,
+        state: TvLazyListState = rememberLazyListState(),
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        reverseLayout: Boolean = false,
+        userScrollEnabled: Boolean = true,
+        spacedBy: Dp = 0.dp,
+        pivotOffsets: PivotOffsets =
+            PivotOffsets(parentFraction = 0f),
+        content: TvLazyListScope.() -> Unit
+    ) {
+        if (vertical) {
+            val verticalArrangement = when {
+                spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+                !reverseLayout -> Arrangement.Top
+                else -> Arrangement.Bottom
+            }
+            TvLazyColumn(
+                modifier = modifier,
+                state = state,
+                contentPadding = contentPadding,
+                reverseLayout = reverseLayout,
+                userScrollEnabled = userScrollEnabled,
+                verticalArrangement = verticalArrangement,
+                pivotOffsets = pivotOffsets,
+                content = content
+            )
+        } else {
+            val horizontalArrangement = when {
+                spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+                !reverseLayout -> Arrangement.Start
+                else -> Arrangement.End
+            }
+            TvLazyRow(
+                modifier = modifier,
+                state = state,
+                contentPadding = contentPadding,
+                reverseLayout = reverseLayout,
+                userScrollEnabled = userScrollEnabled,
+                horizontalArrangement = horizontalArrangement,
+                pivotOffsets = pivotOffsets,
+                content = content
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt
new file mode 100644
index 0000000..f960ffa
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt
@@ -0,0 +1,612 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyArrangementsTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+    private var smallerItemSize: Dp = Dp.Infinity
+    private var containerSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+        with(rule.density) {
+            smallerItemSize = 40.toDp()
+        }
+        containerSize = itemSize * 5
+    }
+
+    // cases when we have not enough items to fill min constraints:
+
+    @Test
+    fun column_defaultArrangementIsTop() {
+        rule.setContent {
+            TvLazyColumn(
+                modifier = Modifier.requiredSize(containerSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Top)
+    }
+
+    @Test
+    fun column_centerArrangement() {
+        composeColumnWith(Arrangement.Center)
+        assertArrangementForTwoItems(Arrangement.Center)
+    }
+
+    @Test
+    fun column_bottomArrangement() {
+        composeColumnWith(Arrangement.Bottom)
+        assertArrangementForTwoItems(Arrangement.Bottom)
+    }
+
+    @Test
+    fun column_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeColumnWith(arrangement)
+        assertArrangementForTwoItems(arrangement)
+    }
+
+    @Test
+    fun row_defaultArrangementIsStart() {
+        rule.setContent {
+            TvLazyRow(
+                modifier = Modifier.requiredSize(containerSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_centerArrangement() {
+        composeRowWith(Arrangement.Center, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_endArrangement() {
+        composeRowWith(Arrangement.End, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeRowWith(arrangement, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_rtl_startArrangement() {
+        composeRowWith(Arrangement.Center, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun row_rtl_endArrangement() {
+        composeRowWith(Arrangement.End, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun row_rtl_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeRowWith(arrangement, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+    }
+
+    // wrap content and spacing
+
+    @Test
+    fun column_spacing_affects_wrap_content() {
+        rule.setContent {
+            TvLazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun row_spacing_affects_wrap_content() {
+        rule.setContent {
+            TvLazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Box(Modifier.requiredSize(itemSize).focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize * 3)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    // spacing added when we have enough items to fill the viewport
+
+    @Test
+    fun column_spacing_scrolledToTheTop() {
+        rule.setContent {
+            TvLazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun column_spacing_scrolledToTheBottom() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    @Test
+    fun row_spacing_scrolledToTheStart() {
+        rule.setContent {
+            TvLazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun row_spacing_scrolledToTheEnd() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(3) {
+                    Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    @Test
+    fun column_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.size(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                verticalArrangement = Arrangement.spacedBy(spacingSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun column_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.size(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                verticalArrangement = Arrangement.spacedBy(spacingSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset)
+                .isEqualTo(itemSizePx + spacingSizePx / 2)
+        }
+    }
+
+    @Test
+    fun row_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                Modifier.size(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                horizontalArrangement = Arrangement.spacedBy(spacingSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(5) {
+                    Box(
+                        Modifier.size(itemSize).testTag("$it").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun row_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+        val itemSizePx = 30
+        val spacingSizePx = 4
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                Modifier.size(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                horizontalArrangement = Arrangement.spacedBy(spacingSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(5) {
+                    Box(
+                        Modifier.size(itemSize).testTag("$it").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset)
+                .isEqualTo(itemSizePx + spacingSizePx / 2)
+        }
+    }
+
+    // with reverseLayout == true
+
+    @Test
+    fun column_defaultArrangementIsBottomWithReverseLayout() {
+        rule.setContent {
+            TvLazyColumn(
+                reverseLayout = true,
+                modifier = Modifier.requiredSize(containerSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+    }
+
+    @Test
+    fun row_defaultArrangementIsEndWithReverseLayout() {
+        rule.setContent {
+            TvLazyRow(
+                reverseLayout = true,
+                modifier = Modifier.requiredSize(containerSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(
+            Arrangement.End, LayoutDirection.Ltr, reverseLayout = true
+        )
+    }
+
+    @Test
+    fun column_whenArrangementChanges() {
+        var arrangement by mutableStateOf(Arrangement.Top)
+        rule.setContent {
+            TvLazyColumn(
+                modifier = Modifier.requiredSize(containerSize),
+                verticalArrangement = arrangement,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Top)
+
+        rule.runOnIdle {
+            arrangement = Arrangement.Bottom
+        }
+
+        assertArrangementForTwoItems(Arrangement.Bottom)
+    }
+
+    @Test
+    fun row_whenArrangementChanges() {
+        var arrangement by mutableStateOf(Arrangement.Start)
+        rule.setContent {
+            TvLazyRow(
+                modifier = Modifier.requiredSize(containerSize),
+                horizontalArrangement = arrangement,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+
+        rule.runOnIdle {
+            arrangement = Arrangement.End
+        }
+
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+    }
+
+    fun composeColumnWith(arrangement: Arrangement.Vertical) {
+        rule.setContent {
+            TvLazyColumn(
+                verticalArrangement = arrangement,
+                modifier = Modifier.requiredSize(containerSize),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
+    }
+
+    fun composeRowWith(arrangement: Arrangement.Horizontal, layoutDirection: LayoutDirection) {
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                TvLazyRow(
+                    horizontalArrangement = arrangement,
+                    modifier = Modifier.requiredSize(containerSize),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(2) {
+                        Item(it)
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        require(index < 2)
+        val size = if (index == 0) itemSize else smallerItemSize
+        Box(Modifier.requiredSize(size).testTag(index.toString()))
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Vertical,
+        reverseLayout: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) {
+                val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+                if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+            }
+            val outPositions = IntArray(2) { 0 }
+            with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+                rule.onNodeWithTag("$realIndex")
+                    .assertTopPositionInRootIsEqualTo(position.toDp())
+            }
+        }
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Horizontal,
+        layoutDirection: LayoutDirection,
+        reverseLayout: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) {
+                val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+                if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+            }
+            val outPositions = IntArray(2) { 0 }
+            with(arrangement) {
+                arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
+            }
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+                val expectedPosition = position.toDp()
+                rule.onNodeWithTag("$realIndex")
+                    .assertLeftPositionInRootIsEqualTo(expectedPosition)
+            }
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt
new file mode 100644
index 0000000..8e8bc51
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt
@@ -0,0 +1,502 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import android.os.Build
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+/**
+ * This class contains all LazyColumn-specific tests, as well as (by convention) tests that don't
+ * need to be run in both orientations.
+ *
+ * To have a test run in both orientations (LazyRow and LazyColumn), add it to [LazyListTest]
+ */
+class LazyColumnTest {
+    private val LazyListTag = "LazyListTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun compositionsAreDisposed_whenDataIsChanged() {
+        var composed = 0
+        var disposals = 0
+        val data1 = (1..3).toList()
+        val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
+
+        var part2 by mutableStateOf(false)
+
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.testTag(LazyListTag).fillMaxSize(),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(if (!part2) data1 else data2) {
+                    DisposableEffect(NeverEqualObject) {
+                        composed++
+                        onDispose {
+                            disposals++
+                        }
+                    }
+
+                    Box(Modifier.height(50.dp).focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("Not all items were composed")
+                .that(composed).isEqualTo(data1.size)
+            composed = 0
+
+            part2 = true
+        }
+
+        rule.runOnIdle {
+            assertWithMessage(
+                "No additional items were composed after data change, something didn't work"
+            ).that(composed).isEqualTo(data2.size)
+
+            // We may need to modify this test once we prefetch/cache items outside the viewport
+            assertWithMessage(
+                "Not enough compositions were disposed after scrolling, compositions were leaked"
+            ).that(disposals).isEqualTo(data1.size)
+        }
+    }
+
+    @Test
+    fun compositionsAreDisposed_whenLazyListIsDisposed() {
+        var emitLazyList by mutableStateOf(true)
+        var disposeCalledOnFirstItem = false
+        var disposeCalledOnSecondItem = false
+
+        rule.setContentWithTestViewConfiguration {
+            if (emitLazyList) {
+                TvLazyColumn(
+                    Modifier.fillMaxSize(),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(2) {
+                        Box(Modifier.requiredSize(100.dp).focusable())
+                        DisposableEffect(Unit) {
+                            onDispose {
+                                if (it == 1) {
+                                    disposeCalledOnFirstItem = true
+                                } else {
+                                    disposeCalledOnSecondItem = true
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First item was incorrectly immediately disposed")
+                .that(disposeCalledOnFirstItem).isFalse()
+            assertWithMessage("Second item was incorrectly immediately disposed")
+                .that(disposeCalledOnFirstItem).isFalse()
+            emitLazyList = false
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First item was not correctly disposed")
+                .that(disposeCalledOnFirstItem).isTrue()
+            assertWithMessage("Second item was not correctly disposed")
+                .that(disposeCalledOnSecondItem).isTrue()
+        }
+    }
+
+    @Test
+    fun removeItemsTest() {
+        val startingNumItems = 3
+        var numItems = startingNumItems
+        var numItemsModel by mutableStateOf(numItems)
+        val tag = "List"
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.testTag(tag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((1..numItemsModel).toList()) {
+                    BasicText("$it")
+                }
+            }
+        }
+
+        while (numItems >= 0) {
+            // Confirm the number of children to ensure there are no extra items
+            rule.onNodeWithTag(tag)
+                .onChildren()
+                .assertCountEquals(numItems)
+
+            // Confirm the children's content
+            for (i in 1..3) {
+                rule.onNodeWithText("$i").apply {
+                    if (i <= numItems) {
+                        assertExists()
+                    } else {
+                        assertDoesNotExist()
+                    }
+                }
+            }
+            numItems--
+            if (numItems >= 0) {
+                // Don't set the model to -1
+                rule.runOnIdle { numItemsModel = numItems }
+            }
+        }
+    }
+
+    @Test
+    fun changeItemsCountAndScrollImmediately() {
+        lateinit var state: TvLazyListState
+        var count by mutableStateOf(100)
+        val composedIndexes = mutableListOf<Int>()
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.fillMaxWidth().height(10.dp),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(count) { index ->
+                    composedIndexes.add(index)
+                    Box(Modifier.size(20.dp).focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            composedIndexes.clear()
+            count = 10
+            runBlocking(AutoTestFrameClock()) {
+                state.scrollToItem(50)
+            }
+            composedIndexes.forEach {
+                assertThat(it).isLessThan(count)
+            }
+            assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+        }
+    }
+
+    @Test
+    fun changingDataTest() {
+        val dataLists = listOf(
+            (1..3).toList(),
+            (4..8).toList(),
+            (3..4).toList()
+        )
+        var dataModel by mutableStateOf(dataLists[0])
+        val tag = "List"
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.testTag(tag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(dataModel) {
+                    BasicText("$it")
+                }
+            }
+        }
+
+        for (data in dataLists) {
+            rule.runOnIdle { dataModel = data }
+
+            // Confirm the number of children to ensure there are no extra items
+            val numItems = data.size
+            rule.onNodeWithTag(tag)
+                .onChildren()
+                .assertCountEquals(numItems)
+
+            // Confirm the children's content
+            for (item in data) {
+                rule.onNodeWithText("$item").assertExists()
+            }
+        }
+    }
+
+    private val firstItemTag = "firstItemTag"
+    private val secondItemTag = "secondItemTag"
+
+    private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                Modifier.testTag(LazyListTag).requiredWidth(100.dp),
+                horizontalAlignment = horizontalGravity,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(listOf(1, 2)) {
+                    if (it == 1) {
+                        Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
+                    } else {
+                        Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertIsDisplayed()
+
+        val lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        with(rule.density) {
+            // Verify the width of the column
+            assertThat(lazyColumnBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+            assertThat(lazyColumnBounds.right.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+        }
+    }
+
+    @Test
+    fun lazyColumnAlignmentCenterHorizontally() {
+        prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(25.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(15.dp, 50.dp)
+    }
+
+    @Test
+    fun lazyColumnAlignmentStart() {
+        prepareLazyColumnsItemsAlignment(Alignment.Start)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+    }
+
+    @Test
+    fun lazyColumnAlignmentEnd() {
+        prepareLazyColumnsItemsAlignment(Alignment.End)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(30.dp, 50.dp)
+    }
+
+    @Test
+    fun removalWithMutableStateListOf() {
+        val items = mutableStateListOf("1", "2", "3")
+
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn {
+                items(items) { item ->
+                    Spacer(Modifier.size(itemSize).testTag(item))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            items.removeLast()
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun recompositionOrder() {
+        val outerState = mutableStateOf(0)
+        val innerState = mutableStateOf(0)
+        val recompositions = mutableListOf<Pair<Int, Int>>()
+
+        rule.setContent {
+            val localOuterState = outerState.value
+            TvLazyColumn {
+                items(count = 1) {
+                    recompositions.add(localOuterState to innerState.value)
+                    Box(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            innerState.value++
+            outerState.value++
+        }
+
+        rule.runOnIdle {
+            assertThat(recompositions).isEqualTo(
+                listOf(0 to 0, 1 to 1)
+            )
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scrolledAwayItemIsNotDisplayedAnymore() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier
+                    .requiredSize(10.dp)
+                    .testTag(LazyListTag)
+                    .graphicsLayer()
+                    .background(Color.Blue),
+                state = state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(2) {
+                    val size = if (it == 0) 5.dp else 100.dp
+                    val color = if (it == 0) Color.Red else Color.Transparent
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(size)
+                            .background(color)
+                            .testTag("$it")
+                            .focusable()
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            with(rule.density) {
+                runBlocking {
+                    // we scroll enough to make the Red item not visible anymore
+                    state.scrollBy(6.dp.toPx())
+                }
+            }
+        }
+
+        // and verify there is no Red item displayed
+        rule.onNodeWithTag(LazyListTag)
+            .captureToImage()
+            .assertPixels {
+                Color.Blue
+            }
+    }
+
+    @Test
+    fun wrappedNestedLazyRowDisplayCorrectContent() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.size(20.dp),
+                state = state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    LazyRowWrapped {
+                        BasicText("$it", Modifier.size(21.dp))
+                    }
+                }
+            }
+        }
+
+        (1..10).forEach { item ->
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollToItem(item)
+                }
+            }
+
+            rule.onNodeWithText("$item")
+                .assertIsDisplayed()
+        }
+    }
+
+    @Composable
+    private fun LazyRowWrapped(content: @Composable () -> Unit) {
+        TvLazyRow {
+            items(count = 1) {
+                content()
+            }
+        }
+    }
+}
+
+internal fun Modifier.drawOutsideOfBounds() = drawBehind {
+    val inflate = 20.dp.roundToPx().toFloat()
+    drawRect(
+        Color.Red,
+        Offset(-inflate, -inflate),
+        Size(size.width + inflate * 2, size.height + inflate * 2)
+    )
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt
new file mode 100644
index 0000000..69d0123
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt
@@ -0,0 +1,491 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyCustomKeysTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val itemSize = with(rule.density) {
+        100.toDp()
+    }
+
+    @Test
+    fun itemsWithKeysAreLaidOutCorrectly() {
+        val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item("${it.id}")
+                }
+            }
+        }
+
+        assertItems("0", "1", "2")
+    }
+
+    @Test
+    fun removing_statesAreMoved() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2])
+        }
+
+        assertItems("0", "2")
+    }
+
+    @Test
+    fun reordering_statesAreMoved_list() {
+        testReordering { list ->
+            items(list, key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_list_indexed() {
+        testReordering { list ->
+            itemsIndexed(list, key = { _, item -> item.id }) { _, item ->
+                Item(remember { "${item.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_array() {
+        testReordering { list ->
+            val array = list.toTypedArray()
+            items(array, key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_array_indexed() {
+        testReordering { list ->
+            val array = list.toTypedArray()
+            itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
+                Item(remember { "${item.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun reordering_statesAreMoved_itemsWithCount() {
+        testReordering { list ->
+            items(list.size, key = { list[it].id }) {
+                Item(remember { "${list[it].id}" })
+            }
+        }
+    }
+
+    @Test
+    fun fullyReplacingTheList() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6))
+        }
+
+        assertItems("3", "4", "5", "6")
+    }
+
+    @Test
+    fun keepingOneItem() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(1))
+        }
+
+        assertItems("1")
+    }
+
+    @Test
+    fun keepingOneItemAndAddingMore() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        var counter = 0
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item(remember { counter++ }.toString())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(MyClass(1), MyClass(3))
+        }
+
+        assertItems("1", "3")
+    }
+
+    @Test
+    fun mixingKeyedItemsAndNot() {
+        testReordering { list ->
+            item {
+                Item("${list.first().id}")
+            }
+            items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
+                Item(remember { "${it.id}" })
+            }
+        }
+    }
+
+    @Test
+    fun updatingTheDataSetIsCorrectlyApplied() {
+        val state = mutableStateOf(emptyList<Int>())
+
+        rule.setContent {
+            LaunchedEffect(Unit) {
+                state.value = listOf(4, 1, 3)
+            }
+
+            val list = state.value
+
+            TvLazyColumn(
+                Modifier.fillMaxSize(),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it }) {
+                    Item(it.toString())
+                }
+            }
+        }
+
+        assertItems("4", "1", "3")
+
+        rule.runOnIdle {
+            state.value = listOf(2, 4, 6, 1, 3, 5)
+        }
+
+        assertItems("2", "4", "6", "1", "3", "5")
+    }
+
+    @Test
+    fun reordering_usingMutableStateListOf() {
+        val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
+
+        rule.setContent {
+            TvLazyColumn {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list.add(list.removeAt(1))
+        }
+
+        assertItems("0", "2", "1")
+    }
+
+    @Test
+    fun keysInLazyListItemInfoAreCorrect() {
+        val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                state = state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(0, 1, 2))
+        }
+    }
+
+    @Test
+    fun keysInLazyListItemInfoAreCorrectAfterReordering() {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                state = state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it.id }) {
+                    Item(remember { "${it.id}" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2], list[1])
+        }
+
+        rule.runOnIdle {
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(0, 2, 1))
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
+        var list by mutableStateOf((10..15).toList())
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.size(itemSize * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeKeepingThisItemFirst() {
+        var list by mutableStateOf((10..15).toList())
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.size(itemSize * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(10, 11, 12))
+        }
+    }
+
+    @Test
+    fun addingItemsRightAfterKeepingThisItemFirst() {
+        var list by mutableStateOf((0..5).toList() + (10..15).toList())
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState(5)
+            TvLazyColumn(
+                Modifier.size(itemSize * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..15).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(5, 6, 7))
+        }
+    }
+
+    @Test
+    fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
+        var list by mutableStateOf((10..30).toList())
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState(10) // key 20 is the first item
+            TvLazyColumn(
+                Modifier.size(itemSize * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..30).toList()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(20)
+            assertThat(
+                state.visibleKeys
+            ).isEqualTo(listOf(20, 21, 22))
+        }
+    }
+
+    @Test
+    fun removingTheCurrentItemMaintainsTheIndex() {
+        var list by mutableStateOf((0..20).toList())
+        lateinit var state: TvLazyListState
+
+        rule.setContent {
+            state = rememberLazyListState(5)
+            TvLazyColumn(
+                Modifier.size(itemSize * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(list, key = { it }) {
+                    Item(remember { "$it" })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = (0..20) - 5
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+            assertThat(state.visibleKeys).isEqualTo(listOf(6, 7, 8))
+        }
+    }
+
+    private fun testReordering(content: TvLazyListScope.(List<MyClass>) -> Unit) {
+        var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+        rule.setContent {
+            TvLazyColumn {
+                content(list)
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(list[0], list[2], list[1])
+        }
+
+        assertItems("0", "2", "1")
+    }
+
+    private fun assertItems(vararg tags: String) {
+        var currentTop = 0.dp
+        tags.forEach {
+            rule.onNodeWithTag(it)
+                .assertTopPositionInRootIsEqualTo(currentTop)
+                .assertHeightIsEqualTo(itemSize)
+            currentTop += itemSize
+        }
+    }
+
+    @Composable
+    private fun Item(tag: String) {
+        Spacer(
+            Modifier.testTag(tag).size(itemSize)
+        )
+    }
+
+    private class MyClass(val id: Int)
+}
+
+val TvLazyListState.visibleKeys: List<Any> get() = layoutInfo.visibleItemsInfo.map { it.key }
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..6a037fb
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun visibleItemsStateRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        restorationTester.setContent {
+            TvLazyColumn {
+                item {
+                    realState[0] = rememberSaveable { counter0++ }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+                items((1..2).toList()) {
+                    if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: TvLazyListState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            TvLazyColumn(
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyListState().also { state = it },
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..10).toList()) {
+                    if (it == 0) {
+                        realState = rememberSaveable { counter0++ }
+                        DisposableEffect(Unit) {
+                            onDispose {
+                                itemDisposed = true
+                            }
+                        }
+                    }
+                    Box(Modifier.requiredSize(30.dp).focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                // we scroll through multiple items to make sure the 0th element is not kept in
+                // the reusable items buffer
+                state.scrollToItem(3)
+                state.scrollToItem(5)
+                state.scrollToItem(8)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        lateinit var state: TvLazyListState
+        var realState = arrayOf(0, 0)
+        restorationTester.setContent {
+            TvLazyColumn(
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyListState().also { state = it },
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..1).toList()) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else {
+                        realState[1] = rememberSaveable { counter1++ }
+                    }
+                    Box(Modifier.requiredSize(30.dp).focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            runBlocking {
+                state.scrollToItem(1, 5)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            realState = arrayOf(0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: TvLazyListState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            TvLazyColumn(
+                Modifier.requiredSize(20.dp),
+                state = rememberLazyListState().also { state = it },
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..10).toList()) {
+                    if (it == 0) {
+                        TvLazyRow {
+                            item {
+                                realState = rememberSaveable { counter0++ }
+                                DisposableEffect(Unit) {
+                                    onDispose {
+                                        itemDisposed = true
+                                    }
+                                }
+                                Box(Modifier.requiredSize(30.dp).focusable())
+                            }
+                        }
+                    } else {
+                        Box(Modifier.requiredSize(30.dp).focusable())
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                // we scroll through multiple items to make sure the 0th element is not kept in
+                // the reusable items buffer
+                state.scrollToItem(3)
+                state.scrollToItem(5)
+                state.scrollToItem(8)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.scrollToItem(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun stateRestoredWhenUsedWithCustomKeys() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        restorationTester.setContent {
+            TvLazyColumn {
+                items(3, key = { "$it" }) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun stateRestoredWhenUsedWithCustomKeysAfterReordering() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        var list by mutableStateOf(listOf(0, 1, 2))
+        restorationTester.setContent {
+            TvLazyColumn {
+                items(list, key = { "$it" }) {
+                    if (it == 0) {
+                        realState[0] = rememberSaveable { counter0++ }
+                    } else if (it == 1) {
+                        realState[1] = rememberSaveable { counter1++ }
+                    } else {
+                        realState[2] = rememberSaveable { counter2++ }
+                    }
+                    Box(Modifier.requiredSize(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2)
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(0)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
new file mode 100644
index 0000000..351bf6f
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -0,0 +1,1226 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.math.roundToInt
+
+@LargeTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyListAnimateItemPlacementTest(private val config: Config) {
+
+    private val isVertical: Boolean get() = config.isVertical
+    private val reverseLayout: Boolean get() = config.reverseLayout
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val itemSize: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private val itemSize2: Int = 30
+    private var itemSize2Dp: Dp = Dp.Infinity
+    private val itemSize3: Int = 20
+    private var itemSize3Dp: Dp = Dp.Infinity
+    private val containerSize: Int = itemSize * 5
+    private var containerSizeDp: Dp = Dp.Infinity
+    private val spacing: Int = 10
+    private var spacingDp: Dp = Dp.Infinity
+    private val itemSizePlusSpacing = itemSize + spacing
+    private var itemSizePlusSpacingDp = Dp.Infinity
+    private lateinit var state: TvLazyListState
+
+    @Before
+    fun before() {
+        rule.mainClock.autoAdvance = false
+        with(rule.density) {
+            itemSizeDp = itemSize.toDp()
+            itemSize2Dp = itemSize2.toDp()
+            itemSize3Dp = itemSize3.toDp()
+            containerSizeDp = containerSize.toDp()
+            spacingDp = spacing.toDp()
+            itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
+        }
+    }
+
+    @Test
+    fun reorderTwoItems() {
+        var list by mutableStateOf(listOf(0, 1))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(0 to 0, 1 to itemSize)
+
+        rule.runOnIdle {
+            list = listOf(1, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSize * fraction).roundToInt(),
+                1 to itemSize - (itemSize * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun reorderTwoItems_layoutInfoHasFinalPositions() {
+        var list by mutableStateOf(listOf(0, 1))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertLayoutInfoPositions(0 to 0, 1 to itemSize)
+
+        rule.runOnIdle {
+            list = listOf(1, 0)
+        }
+
+        onAnimationFrame {
+            // fraction doesn't affect the offsets in layout info
+            assertLayoutInfoPositions(1 to 0, 0 to itemSize)
+        }
+    }
+
+    @Test
+    fun reorderFirstAndLastItems() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to 0,
+            1 to itemSize,
+            2 to itemSize * 2,
+            3 to itemSize * 3,
+            4 to itemSize * 4,
+        )
+
+        rule.runOnIdle {
+            list = listOf(4, 1, 2, 3, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSize * 4 * fraction).roundToInt(),
+                1 to itemSize,
+                2 to itemSize * 2,
+                3 to itemSize * 3,
+                4 to itemSize * 4 - (itemSize * 4 * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveFirstItemToEndCausingAllItemsToAnimate() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to 0,
+            1 to itemSize,
+            2 to itemSize * 2,
+            3 to itemSize * 3,
+            4 to itemSize * 4,
+        )
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSize * 4 * fraction).roundToInt(),
+                1 to itemSize - (itemSize * fraction).roundToInt(),
+                2 to itemSize * 2 - (itemSize * fraction).roundToInt(),
+                3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+                4 to itemSize * 4 - (itemSize * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun itemSizeChangeAnimatesNextItems() {
+        var size by mutableStateOf(itemSizeDp)
+        rule.setContent {
+            LazyList(
+                minSize = itemSizeDp * 5,
+                maxSize = itemSizeDp * 5
+            ) {
+                items(listOf(0, 1, 2, 3), key = { it }) {
+                    Item(it, size = if (it == 1) size else itemSizeDp)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            size = itemSizeDp * 2
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithTag("1")
+            .assertMainAxisSizeIsEqualTo(size)
+
+        onAnimationFrame { fraction ->
+            if (!reverseLayout) {
+                assertPositions(
+                    0 to 0,
+                    1 to itemSize,
+                    2 to itemSize * 2 + (itemSize * fraction).roundToInt(),
+                    3 to itemSize * 3 + (itemSize * fraction).roundToInt(),
+                    fraction = fraction,
+                    autoReverse = false
+                )
+            } else {
+                assertPositions(
+                    3 to itemSize - (itemSize * fraction).roundToInt(),
+                    2 to itemSize * 2 - (itemSize * fraction).roundToInt(),
+                    1 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+                    0 to itemSize * 4,
+                    fraction = fraction,
+                    autoReverse = false
+                )
+            }
+        }
+    }
+
+    @Test
+    fun onlyItemsWithModifierAnimates() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to itemSize * 4,
+                1 to itemSize - (itemSize * fraction).roundToInt(),
+                2 to itemSize,
+                3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+                4 to itemSize * 3,
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animationsWithDifferentDurations() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    val duration = if (it == 1 || it == 3) Duration * 2 else Duration
+                    Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 4, 0)
+        }
+
+        onAnimationFrame(duration = Duration * 2) { fraction ->
+            val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
+            assertPositions(
+                0 to 0 + (itemSize * 4 * shorterAnimFraction).roundToInt(),
+                1 to itemSize - (itemSize * fraction).roundToInt(),
+                2 to itemSize * 2 - (itemSize * shorterAnimFraction).roundToInt(),
+                3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+                4 to itemSize * 4 - (itemSize * shorterAnimFraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun multipleChildrenPerItem() {
+        var list by mutableStateOf(listOf(0, 2))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                    Item(it + 1)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to 0,
+            1 to itemSize,
+            2 to itemSize * 2,
+            3 to itemSize * 3,
+        )
+
+        rule.runOnIdle {
+            list = listOf(2, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSize * 2 * fraction).roundToInt(),
+                1 to itemSize + (itemSize * 2 * fraction).roundToInt(),
+                2 to itemSize * 2 - (itemSize * 2 * fraction).roundToInt(),
+                3 to itemSize * 3 - (itemSize * 2 * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun multipleChildrenPerItemSomeDoNotAnimate() {
+        var list by mutableStateOf(listOf(0, 2))
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                    Item(it + 1, animSpec = null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(2, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSize * 2 * fraction).roundToInt(),
+                1 to itemSize * 3,
+                2 to itemSize * 2 - (itemSize * 2 * fraction).roundToInt(),
+                3 to itemSize,
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animateArrangementChange() {
+        var arrangement by mutableStateOf(Arrangement.Center)
+        rule.setContent {
+            LazyList(
+                arrangement = arrangement,
+                minSize = itemSizeDp * 5,
+                maxSize = itemSizeDp * 5
+            ) {
+                items(listOf(1, 2, 3), key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            1 to itemSize,
+            2 to itemSize * 2,
+            3 to itemSize * 3,
+        )
+
+        rule.runOnIdle {
+            arrangement = Arrangement.SpaceBetween
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                1 to itemSize - (itemSize * fraction).roundToInt(),
+                2 to itemSize * 2,
+                3 to itemSize * 3 + (itemSize * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        rule.setContent {
+            LazyList(maxSize = itemSizeDp * 3) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to 0,
+            1 to itemSize,
+            2 to itemSize * 2
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 4, 2, 3, 1, 5)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset = itemSize + (itemSize * 3 * fraction).roundToInt()
+            val item4Offset = itemSize * 4 - (itemSize * 3 * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                add(0 to 0)
+                if (item1Offset < itemSize * 3) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(2 to itemSize * 2)
+                if (item4Offset < itemSize * 3) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        rule.setContent {
+            LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            3 to 0,
+            4 to itemSize,
+            5 to itemSize * 2
+        )
+
+        rule.runOnIdle {
+            list = listOf(2, 4, 0, 3, 1, 5)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset = itemSize * -2 + (itemSize * 3 * fraction).roundToInt()
+            val item4Offset = itemSize - (itemSize * 3 * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                if (item4Offset > -itemSize) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+                add(3 to 0)
+                if (item1Offset > -itemSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(5 to itemSize * 2)
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyList(arrangement = Arrangement.spacedBy(spacingDp)) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(1, 2, 3, 0)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to 0 + (itemSizePlusSpacing * 3 * fraction).roundToInt(),
+                1 to itemSizePlusSpacing - (itemSizePlusSpacing * fraction).roundToInt(),
+                2 to itemSizePlusSpacing * 2 - (itemSizePlusSpacing * fraction).roundToInt(),
+                3 to itemSizePlusSpacing * 3 - (itemSizePlusSpacing * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        rule.setContent {
+            LazyList(
+                maxSize = itemSizeDp * 3 + spacingDp * 2,
+                arrangement = Arrangement.spacedBy(spacingDp)
+            ) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            0 to 0,
+            1 to itemSizePlusSpacing,
+            2 to itemSizePlusSpacing * 2
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 4, 2, 3, 1, 5)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset =
+                itemSizePlusSpacing + (itemSizePlusSpacing * 3 * fraction).roundToInt()
+            val item4Offset =
+                itemSizePlusSpacing * 4 - (itemSizePlusSpacing * 3 * fraction).roundToInt()
+            val screenSize = itemSize * 3 + spacing * 2
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                add(0 to 0)
+                if (item1Offset < screenSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(2 to itemSizePlusSpacing * 2)
+                if (item4Offset < screenSize) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds_withSpacing() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
+        rule.setContent {
+            LazyList(
+                maxSize = itemSizeDp * 3 + spacingDp * 2,
+                startIndex = 3,
+                arrangement = Arrangement.spacedBy(spacingDp)
+            ) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertPositions(
+            3 to 0,
+            4 to itemSizePlusSpacing,
+            5 to itemSizePlusSpacing * 2
+        )
+
+        rule.runOnIdle {
+            list = listOf(2, 4, 0, 3, 1, 5, 6, 7)
+        }
+
+        onAnimationFrame { fraction ->
+            val item1Offset =
+                itemSizePlusSpacing * -2 + (itemSizePlusSpacing * 3 * fraction).roundToInt()
+            val item4Offset =
+                (itemSizePlusSpacing - itemSizePlusSpacing * 3 * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                if (item4Offset > -itemSize) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+                add(3 to 0)
+                if (item1Offset > -itemSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(5 to itemSizePlusSpacing * 2)
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheTopOutsideOfBounds_differentSizes() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        rule.setContent {
+            LazyList(maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 3) {
+                items(list, key = { it }) {
+                    val size =
+                        if (it == 3) itemSize2Dp else if (it == 1) itemSize3Dp else itemSizeDp
+                    Item(it, size = size)
+                }
+            }
+        }
+
+        val item3Size = itemSize2
+        val item4Size = itemSize
+        assertPositions(
+            3 to 0,
+            4 to item3Size,
+            5 to item3Size + item4Size
+        )
+
+        rule.runOnIdle {
+            // swap 4 and 1
+            list = listOf(0, 4, 2, 3, 1, 5)
+        }
+
+        onAnimationFrame { fraction ->
+            rule.onNodeWithTag("2").assertDoesNotExist()
+            // item 2 was between 1 and 3 but we don't compose it and don't know the real size,
+            // so we use an average size.
+            val item2Size = (itemSize + itemSize2 + itemSize3) / 3
+            val item1Size = itemSize3 /* the real size of the item 1 */
+            val startItem1Offset = -item1Size - item2Size
+            val item1Offset =
+                startItem1Offset + ((itemSize2 - startItem1Offset) * fraction).roundToInt()
+            val endItem4Offset = -item4Size - item2Size
+            val item4Offset = item3Size - ((item3Size - endItem4Offset) * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                if (item4Offset > -item4Size) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+                add(3 to 0)
+                if (item1Offset > -item1Size) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(5 to item3Size + item4Size - ((item4Size - item1Size) * fraction).roundToInt())
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+        val listSize = itemSize2 + itemSize3 + itemSize - 1
+        val listSizeDp = with(rule.density) { listSize.toDp() }
+        rule.setContent {
+            LazyList(maxSize = listSizeDp) {
+                items(list, key = { it }) {
+                    val size =
+                        if (it == 0) itemSize2Dp else if (it == 4) itemSize3Dp else itemSizeDp
+                    Item(it, size = size)
+                }
+            }
+        }
+
+        val item0Size = itemSize2
+        val item1Size = itemSize
+        assertPositions(
+            0 to 0,
+            1 to item0Size,
+            2 to item0Size + item1Size
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 4, 2, 3, 1, 5)
+        }
+
+        onAnimationFrame { fraction ->
+            val item2Size = itemSize
+            val item4Size = itemSize3
+            // item 3 was between 2 and 4 but we don't compose it and don't know the real size,
+            // so we use an average size.
+            val item3Size = (itemSize + itemSize2 + itemSize3) / 3
+            val startItem4Offset = item0Size + item1Size + item2Size + item3Size
+            val endItem1Offset = item0Size + item4Size + item2Size + item3Size
+            val item1Offset =
+                item0Size + ((endItem1Offset - item0Size) * fraction).roundToInt()
+            val item4Offset =
+                startItem4Offset - ((startItem4Offset - item0Size) * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                add(0 to 0)
+                if (item1Offset < listSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+                add(2 to item0Size + item1Size - ((item1Size - item4Size) * fraction).roundToInt())
+                if (item4Offset < listSize) {
+                    add(4 to item4Offset)
+                } else {
+                    rule.onNodeWithTag("4").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animateAlignmentChange() {
+        var alignment by mutableStateOf(CrossAxisAlignment.End)
+        rule.setContent {
+            LazyList(
+                crossAxisAlignment = alignment,
+                crossAxisSize = itemSizeDp
+            ) {
+                items(listOf(1, 2, 3), key = { it }) {
+                    val crossAxisSize =
+                        if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+                    Item(it, crossAxisSize = crossAxisSize)
+                }
+            }
+        }
+
+        val item2Start = itemSize - itemSize2
+        val item3Start = itemSize - itemSize3
+        assertPositions(
+            1 to 0,
+            2 to itemSize,
+            3 to itemSize * 2,
+            crossAxis = listOf(
+                1 to 0,
+                2 to item2Start,
+                3 to item3Start,
+            )
+        )
+
+        rule.runOnIdle {
+            alignment = CrossAxisAlignment.Center
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        val item2End = itemSize / 2 - itemSize2 / 2
+        val item3End = itemSize / 2 - itemSize3 / 2
+        onAnimationFrame { fraction ->
+            assertPositions(
+                1 to 0,
+                2 to itemSize,
+                3 to itemSize * 2,
+                crossAxis = listOf(
+                    1 to 0,
+                    2 to item2Start + ((item2End - item2Start) * fraction).roundToInt(),
+                    3 to item3Start + ((item3End - item3Start) * fraction).roundToInt(),
+                ),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animateAlignmentChange_multipleChildrenPerItem() {
+        var alignment by mutableStateOf(CrossAxisAlignment.Start)
+        rule.setContent {
+            LazyList(
+                crossAxisAlignment = alignment,
+                crossAxisSize = itemSizeDp * 2
+            ) {
+                items(1) {
+                    listOf(1, 2, 3).forEach {
+                        val crossAxisSize =
+                            if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+                        Item(it, crossAxisSize = crossAxisSize)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            alignment = CrossAxisAlignment.End
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        val containerSize = itemSize * 2
+        onAnimationFrame { fraction ->
+            assertPositions(
+                1 to 0,
+                2 to itemSize,
+                3 to itemSize * 2,
+                crossAxis = listOf(
+                    1 to ((containerSize - itemSize) * fraction).roundToInt(),
+                    2 to ((containerSize - itemSize2) * fraction).roundToInt(),
+                    3 to ((containerSize - itemSize3) * fraction).roundToInt()
+                ),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animateAlignmentChange_rtl() {
+        // this test is not applicable to LazyRow
+        assumeTrue(isVertical)
+
+        var alignment by mutableStateOf(CrossAxisAlignment.End)
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                LazyList(
+                    crossAxisAlignment = alignment,
+                    crossAxisSize = itemSizeDp
+                ) {
+                    items(listOf(1, 2, 3), key = { it }) {
+                        val crossAxisSize =
+                            if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+                        Item(it, crossAxisSize = crossAxisSize)
+                    }
+                }
+            }
+        }
+
+        assertPositions(
+            1 to 0,
+            2 to itemSize,
+            3 to itemSize * 2,
+            crossAxis = listOf(
+                1 to 0,
+                2 to 0,
+                3 to 0,
+            )
+        )
+
+        rule.runOnIdle {
+            alignment = CrossAxisAlignment.Center
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                1 to 0,
+                2 to itemSize,
+                3 to itemSize * 2,
+                crossAxis = listOf(
+                    1 to 0,
+                    2 to ((itemSize / 2 - itemSize2 / 2) * fraction).roundToInt(),
+                    3 to ((itemSize / 2 - itemSize3 / 2) * fraction).roundToInt(),
+                ),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        val rawStartPadding = 8
+        val rawEndPadding = 12
+        val (startPaddingDp, endPaddingDp) = with(rule.density) {
+            rawStartPadding.toDp() to rawEndPadding.toDp()
+        }
+        rule.setContent {
+            LazyList(startPadding = startPaddingDp, endPadding = endPaddingDp) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
+        assertPositions(
+            0 to startPadding,
+            1 to startPadding + itemSize,
+            2 to startPadding + itemSize * 2,
+            3 to startPadding + itemSize * 3,
+            4 to startPadding + itemSize * 4,
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 4, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to startPadding,
+                1 to startPadding + itemSize + (itemSize * 3 * fraction).roundToInt(),
+                2 to startPadding + itemSize * 2 - (itemSize * fraction).roundToInt(),
+                3 to startPadding + itemSize * 3 - (itemSize * fraction).roundToInt(),
+                4 to startPadding + itemSize * 4 - (itemSize * fraction).roundToInt(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+
+        var measurePasses = 0
+        rule.setContent {
+            LazyList {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+            LaunchedEffect(Unit) {
+                snapshotFlow { state.layoutInfo }
+                    .collect {
+                        measurePasses++
+                    }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(4, 1, 2, 3, 0)
+        }
+
+        var startMeasurePasses = Int.MIN_VALUE
+        onAnimationFrame { fraction ->
+            if (fraction == 0f) {
+                startMeasurePasses = measurePasses
+            }
+        }
+        rule.mainClock.advanceTimeByFrame()
+        // new layoutInfo is produced on every remeasure of Lazy lists.
+        // but we want to avoid remeasuring and only do relayout on each animation frame.
+        // two extra measures are possible as we switch inProgress flag.
+        assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
+    }
+
+    @Test
+    fun noAnimationWhenScrollOtherPosition() {
+        rule.setContent {
+            LazyList(maxSize = itemSizeDp * 3) {
+                items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0, itemSize / 2)
+            }
+        }
+
+        onAnimationFrame { fraction ->
+            assertPositions(
+                0 to -itemSize / 2,
+                1 to itemSize / 2,
+                2 to itemSize * 3 / 2,
+                3 to itemSize * 5 / 2,
+                fraction = fraction
+            )
+        }
+    }
+
+    private fun assertPositions(
+        vararg expected: Pair<Any, Int>,
+        crossAxis: List<Pair<Any, Int>>? = null,
+        fraction: Float? = null,
+        autoReverse: Boolean = reverseLayout
+    ) {
+        with(rule.density) {
+            val actual = expected.map {
+                val actualOffset = rule.onNodeWithTag(it.first.toString())
+                    .getUnclippedBoundsInRoot().let { bounds ->
+                        val offset = if (isVertical) bounds.top else bounds.left
+                        if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+                    }
+                it.first to actualOffset
+            }
+            val subject = if (fraction == null) {
+                assertThat(actual)
+            } else {
+                assertWithMessage("Fraction=$fraction").that(actual)
+            }
+            subject.isEqualTo(
+                listOf(*expected).let { list ->
+                    if (!autoReverse) {
+                        list
+                    } else {
+                        val containerBounds = rule.onNodeWithTag(ContainerTag).getBoundsInRoot()
+                        val mainAxisSize =
+                            if (isVertical) containerBounds.height else containerBounds.width
+                        val mainAxisSizePx = with(rule.density) { mainAxisSize.roundToPx() }
+                        list.map {
+                            val itemSize = rule.onNodeWithTag(it.first.toString())
+                                .getUnclippedBoundsInRoot().let { bounds ->
+                                    (if (isVertical) bounds.height else bounds.width).roundToPx()
+                                }
+                            it.first to (mainAxisSizePx - itemSize - it.second)
+                        }
+                    }
+                }
+            )
+            if (crossAxis != null) {
+                val actualCross = expected.map {
+                    val actualOffset = rule.onNodeWithTag(it.first.toString())
+                        .getUnclippedBoundsInRoot().let { bounds ->
+                            val offset = if (isVertical) bounds.left else bounds.top
+                            if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+                        }
+                    it.first to actualOffset
+                }
+                assertWithMessage(
+                    "CrossAxis" + if (fraction != null) "for fraction=$fraction" else ""
+                )
+                    .that(actualCross)
+                    .isEqualTo(crossAxis)
+            }
+        }
+    }
+
+    private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, Int>) {
+        rule.runOnIdle {
+            assertThat(visibleItemsOffsets).isEqualTo(listOf(*offsets))
+        }
+    }
+
+    private val visibleItemsOffsets: List<Pair<Any, Int>>
+        get() = state.layoutInfo.visibleItemsInfo.map {
+            it.key to it.offset
+        }
+
+    private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+        require(duration.mod(FrameDuration) == 0L)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            onFrame(i / duration.toFloat())
+            rule.mainClock.advanceTimeBy(FrameDuration)
+            expectedTime += FrameDuration
+            assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            rule.waitForIdle()
+        }
+    }
+
+    @Composable
+    private fun LazyList(
+        arrangement: Arrangement.HorizontalOrVertical? = null,
+        minSize: Dp = 0.dp,
+        maxSize: Dp = containerSizeDp,
+        startIndex: Int = 0,
+        crossAxisSize: Dp = Dp.Unspecified,
+        crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Start,
+        startPadding: Dp = 0.dp,
+        endPadding: Dp = 0.dp,
+        content: TvLazyListScope.() -> 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
+            }
+            TvLazyColumn(
+                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),
+                pivotOffsets = PivotOffsets(parentFraction = 0f),
+                content = content
+            )
+        } 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
+            } else {
+                Alignment.Bottom
+            }
+            TvLazyRow(
+                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),
+                pivotOffsets = PivotOffsets(parentFraction = 0f),
+                content = content
+            )
+        }
+    }
+
+    @Composable
+    private fun LazyItemScope.Item(
+        tag: Int,
+        size: Dp = itemSizeDp,
+        crossAxisSize: Dp = size,
+        animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
+    ) {
+        Box(
+            Modifier
+                .then(
+                    if (isVertical) {
+                        Modifier.requiredHeight(size).requiredWidth(crossAxisSize)
+                    } else {
+                        Modifier.requiredWidth(size).requiredHeight(crossAxisSize)
+                    }
+                )
+                .testTag(tag.toString())
+                .then(
+                    if (animSpec != null) {
+                        Modifier.animateItemPlacement(animSpec)
+                    } else {
+                        Modifier
+                    }
+                )
+        )
+    }
+
+    private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
+        expected: Dp
+    ): SemanticsNodeInteraction {
+        return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
+    }
+
+    companion object {
+        @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),
+        )
+
+        class Config(
+            val isVertical: Boolean,
+            val reverseLayout: Boolean
+        ) {
+            override fun toString() =
+                (if (isVertical) "LazyColumn" else "LazyRow") +
+                    (if (reverseLayout) "(reverse)" else "")
+        }
+    }
+}
+
+private val FrameDuration = 16L
+private val Duration = 400L
+private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
+private val ContainerTag = "container"
+
+private enum class CrossAxisAlignment {
+    Start,
+    End,
+    Center
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
new file mode 100644
index 0000000..5e39177
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
@@ -0,0 +1,461 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyListLayoutInfoTest(
+    param: LayoutInfoTestParam
+) : BaseLazyListTestWithOrientation(param.orientation) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            LayoutInfoTestParam(Orientation.Vertical, false),
+            LayoutInfoTestParam(Orientation.Vertical, true),
+            LayoutInfoTestParam(Orientation.Horizontal, false),
+            LayoutInfoTestParam(Orientation.Horizontal, true),
+        )
+    }
+
+    private val reverseLayout = param.reverseLayout
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 4)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScroll() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1, 10)
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1, startOffset = -10)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithSpacing() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                spacedBy = itemSizeDp,
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx)
+        }
+    }
+
+    @Composable
+    fun ObservingFun(state: TvLazyListState, currentInfo: StableRef<LazyListLayoutInfo?>) {
+        currentInfo.value = state.layoutInfo
+    }
+    @Test
+    fun visibleItemsAreObservableWhenWeScroll() {
+        lateinit var state: TvLazyListState
+        val currentInfo = StableRef<LazyListLayoutInfo?>(null)
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingFun(state, currentInfo)
+        }
+
+        rule.runOnIdle {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            runBlocking {
+                state.scrollToItem(1, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: TvLazyListState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: LazyListLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyColumnOrRow(
+                reverseLayout = reverseLayout,
+                state = rememberLazyListState().also { state = it }
+            ) {
+                item {
+                    Box(Modifier.requiredSize(size))
+                }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                reverseLayout = reverseLayout,
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0 until count).toList()) {
+                    Box(Modifier.requiredSize(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                reverseLayout = reverseLayout,
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.requiredSize(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp,
+                    beforeContentCrossAxis = 2.dp,
+                    afterContentCrossAxis = 2.dp
+                ),
+                reverseLayout = reverseLayout,
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.requiredSize(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun emptyItemsInVisibleItemsInfo() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it }
+            ) {
+                item { Box(Modifier) }
+                item { }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
+            assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun emptyContent() {
+        lateinit var state: TvLazyListState
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        rule.setContent {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp
+                )
+            ) {
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun viewportIsLargerThenTheContent() {
+        lateinit var state: TvLazyListState
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val beforeContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+        }
+        val afterContentPaddingDp = with(rule.density) {
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+        }
+        rule.setContent {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                contentPadding = PaddingValues(
+                    beforeContent = beforeContentPaddingDp,
+                    afterContent = afterContentPaddingDp
+                )
+            ) {
+                item {
+                    Box(Modifier.size(sizeDp / 2))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(
+                if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+            )
+        }
+    }
+
+    @Test
+    fun reverseLayoutIsCorrect() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                reverseLayout = reverseLayout,
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
+        }
+    }
+
+    @Test
+    fun orientationIsCorrect() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.orientation)
+                .isEqualTo(if (vertical) Orientation.Vertical else Orientation.Horizontal)
+        }
+    }
+
+    fun LazyListLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        startOffset: Int = 0,
+        expectedSize: Int = itemSizePx,
+        spacing: Int = 0
+    ) {
+        assertThat(visibleItemsInfo.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var currentOffset = startOffset
+        visibleItemsInfo.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertWithMessage("Offset of item $currentIndex").that(it.offset)
+                .isEqualTo(currentOffset)
+            assertThat(it.size).isEqualTo(expectedSize)
+            currentIndex++
+            currentOffset += it.size + spacing
+        }
+    }
+}
+
+class LayoutInfoTestParam(
+    val orientation: Orientation,
+    val reverseLayout: Boolean
+) {
+    override fun toString(): String {
+        return "orientation=$orientation;reverseLayout=$reverseLayout"
+    }
+}
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
new file mode 100644
index 0000000..db1d248
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListPrefetcherTest(
+    orientation: Orientation
+) : BaseLazyListTestWithOrientation(orientation) {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            Orientation.Vertical,
+            Orientation.Horizontal,
+        )
+    }
+
+    val itemsSizePx = 30
+    val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+    lateinit var state: TvLazyListState
+
+    @Test
+    fun notPrefetchingForwardInitially() {
+        composeList()
+
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun notPrefetchingBackwardInitially() {
+        composeList(firstItem = 2)
+
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAfterSmallScroll() {
+        composeList()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(2)
+
+        rule.onNodeWithTag("2")
+            .assertExists()
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingBackwardAfterSmallScroll() {
+        composeList(firstItem = 2, itemOffset = 10)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-5f)
+            }
+        }
+
+        waitForPrefetch(1)
+
+        rule.onNodeWithTag("1")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackward() {
+        composeList(firstItem = 1)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(3)
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+                state.scrollBy(-1f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardTwice() {
+        composeList()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(2)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemsSizePx / 2f)
+                state.scrollBy(itemsSizePx / 2f)
+            }
+        }
+
+        waitForPrefetch(3)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("3")
+            .assertExists()
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingBackwardTwice() {
+        composeList(firstItem = 4)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-5f)
+            }
+        }
+
+        waitForPrefetch(2)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-itemsSizePx / 2f)
+                state.scrollBy(-itemsSizePx / 2f)
+            }
+        }
+
+        waitForPrefetch(1)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackwardReverseLayout() {
+        composeList(firstItem = 1, reverseLayout = true)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(3)
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+                state.scrollBy(-1f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun prefetchingForwardAndBackwardWithContentPadding() {
+        val halfItemSize = itemsSizeDp / 2f
+        composeList(
+            firstItem = 2,
+            itemOffset = 5,
+            contentPadding = PaddingValues(mainAxis = halfItemSize)
+        )
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(3)
+
+        rule.onNodeWithTag("4")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+            }
+        }
+
+        waitForPrefetch(0)
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+    }
+
+    @Test
+    fun disposingWhilePrefetchingScheduled() {
+        var emit = true
+        lateinit var remeasure: Remeasurement
+        rule.setContent {
+            SubcomposeLayout(
+                modifier = object : RemeasurementModifier {
+                    override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+                        remeasure = remeasurement
+                    }
+                }
+            ) { constraints ->
+                val placeable = if (emit) {
+                    subcompose(Unit) {
+                        state = rememberLazyListState()
+                        LazyColumnOrRow(
+                            Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+                            state,
+                        ) {
+                            items(1000) {
+                                Spacer(
+                                    Modifier
+                                        .mainAxisSize(itemsSizeDp)
+                                        .then(fillParentMaxCrossAxis())
+                                )
+                            }
+                        }
+                    }.first().measure(constraints)
+                } else {
+                    null
+                }
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeable?.place(0, 0)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // this will schedule the prefetching
+            runBlocking(AutoTestFrameClock()) {
+                state.scrollBy(itemsSizePx.toFloat())
+            }
+            // then we synchronously dispose LazyColumn
+            emit = false
+            remeasure.forceRemeasure()
+        }
+
+        rule.runOnIdle { }
+    }
+
+    private fun waitForPrefetch(index: Int) {
+        rule.waitUntil {
+            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+        }
+    }
+
+    private val activeNodes = mutableSetOf<Int>()
+    private val activeMeasuredNodes = mutableSetOf<Int>()
+
+    private fun composeList(
+        firstItem: Int = 0,
+        itemOffset: Int = 0,
+        reverseLayout: Boolean = false,
+        contentPadding: PaddingValues = PaddingValues(0.dp)
+    ) {
+        rule.setContent {
+            state = rememberLazyListState(
+                initialFirstVisibleItemIndex = firstItem,
+                initialFirstVisibleItemScrollOffset = itemOffset
+            )
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+                state,
+                reverseLayout = reverseLayout,
+                contentPadding = contentPadding
+            ) {
+                items(100) {
+                    DisposableEffect(it) {
+                        activeNodes.add(it)
+                        onDispose {
+                            activeNodes.remove(it)
+                            activeMeasuredNodes.remove(it)
+                        }
+                    }
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(itemsSizeDp)
+                            .fillMaxCrossAxis()
+                            .testTag("$it")
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                activeMeasuredNodes.add(it)
+                                layout(placeable.width, placeable.height) {
+                                    placeable.place(0, 0)
+                                }
+                            }
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
new file mode 100644
index 0000000..7e0f810
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
@@ -0,0 +1,506 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListSlotsReuseTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val itemsSizePx = 30f
+    val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+    @Test
+    fun scroll1ItemScrolledOffItemIsKeptForReuse() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1)
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun checkMaxItemsKeptForReuse() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(DefaultMaxItemsToRetain + 1)
+            }
+        }
+
+        repeat(DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$it")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                // after this step 0 and 1 are in reusable buffer
+                state.scrollToItem(2)
+
+                // this step requires one item and will take the last item from the buffer - item
+                // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
+                state.scrollToItem(3)
+            }
+        }
+
+        // recycled
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        // in buffer
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("2")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun doMultipleScrollsOneByOne() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1) // buffer is [0]
+                state.scrollToItem(2) // 0 used, buffer is [1]
+                state.scrollToItem(3) // 1 used, buffer is [2]
+                state.scrollToItem(4) // 2 used, buffer is [3]
+            }
+        }
+
+        // recycled
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+
+        // in buffer
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollBackwardOnce() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState(10)
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(8) // buffer is [10, 11]
+            }
+        }
+
+        // in buffer
+        rule.onNodeWithTag("10")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("11")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("8")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("9")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollBackwardOneByOne() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState(10)
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                            .focusable())
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(9) // buffer is [11]
+                state.scrollToItem(7) // 11 reused, buffer is [9]
+                state.scrollToItem(6) // 9 reused, buffer is [8]
+            }
+        }
+
+        // in buffer
+        rule.onNodeWithTag("8")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        // visible
+        rule.onNodeWithTag("6")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("7")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollingBackReusesTheSameSlot() {
+        lateinit var state: TvLazyListState
+        var counter0 = 0
+        var counter1 = 10
+        var rememberedValue0 = -1
+        var rememberedValue1 = -1
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 1.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(100) {
+                    if (it == 0) {
+                        rememberedValue0 = remember { counter0++ }
+                    }
+                    if (it == 1) {
+                        rememberedValue1 = remember { counter1++ }
+                    }
+                    Box(
+                        Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+                        .focusable())
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2) // buffer is [0, 1]
+                state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
+                .that(rememberedValue0).isEqualTo(0)
+            Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
+                .that(rememberedValue1).isEqualTo(10)
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+    }
+
+    @Test
+    fun differentContentTypes() {
+        lateinit var state: TvLazyListState
+        val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
+        val startOfType1 = DefaultMaxItemsToRetain + 1
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(
+                    100,
+                    contentType = { if (it >= startOfType1) 1 else 0 }
+                ) {
+                    Box(
+                        Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it").focusable())
+                }
+            }
+        }
+
+        for (i in 0 until visibleItemsCount) {
+            rule.onNodeWithTag("$i")
+                .assertIsDisplayed()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(visibleItemsCount)
+            }
+        }
+
+        rule.onNodeWithTag("$visibleItemsCount")
+            .assertIsDisplayed()
+
+        // [DefaultMaxItemsToRetain] items of type 0 are left for reuse
+        for (i in 0 until DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$i")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+            .assertDoesNotExist()
+
+        // and 7 items of type 1
+        for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
+            rule.onNodeWithTag("$i")
+                .assertExists()
+                .assertIsNotDisplayed()
+        }
+        rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun differentTypesFromDifferentItemCalls() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            TvLazyColumn(
+                Modifier.height(itemsSizeDp * 2.5f),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                val content = @Composable { tag: String ->
+                    Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag).focusable())
+                }
+                item(contentType = "not-to-reuse-0") {
+                    content("0")
+                }
+                item(contentType = "reuse") {
+                    content("1")
+                }
+                items(
+                    List(100) { it + 2 },
+                    contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }) {
+                    content("$it")
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2)
+                // now items 0 and 1 are put into reusables
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(9)
+                // item 10 should reuse slot 1
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("9")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("10")
+            .assertIsDisplayed()
+        rule.onNodeWithTag("11")
+            .assertIsDisplayed()
+    }
+}
+
+private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt
new file mode 100644
index 0000000..6b61bf4
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt
@@ -0,0 +1,1733 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredSizeIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.WithTouchSlop
+import androidx.compose.testutils.assertPixels
+import androidx.compose.testutils.assertShape
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyNotDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
+    private val LazyListTag = "LazyListTag"
+    private val firstItemTag = "firstItemTag"
+
+    @Test
+    fun lazyListShowsCombinedItems() {
+        val itemTestTag = "itemTestTag"
+        val items = listOf(1, 2).map { it.toString() }
+        val indexedItems = listOf(3, 4, 5)
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
+                item {
+                    Spacer(
+                        Modifier.mainAxisSize(40.dp)
+                            .then(fillParentMaxCrossAxis())
+                            .testTag(itemTestTag)
+                    )
+                }
+                items(items) {
+                    Spacer(Modifier.mainAxisSize(40.dp).then(fillParentMaxCrossAxis()).testTag(it))
+                }
+                itemsIndexed(indexedItems) { index, item ->
+                    Spacer(
+                        Modifier.mainAxisSize(41.dp).then(fillParentMaxCrossAxis())
+                            .testTag("$index-$item")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(itemTestTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("0-3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-4")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-5")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyListAllowEmptyListItems() {
+        val itemTag = "itemTag"
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow {
+                items(emptyList<Any>()) { }
+                item {
+                    Spacer(Modifier.size(10.dp).testTag(itemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(itemTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyListAllowsNullableItems() {
+        val items = listOf("1", null, "3")
+        val nullTestTag = "nullTestTag"
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
+                items(items) {
+                    if (it != null) {
+                        Spacer(
+                            Modifier.mainAxisSize(101.dp)
+                                .then(fillParentMaxCrossAxis())
+                                .testTag(it)
+                        )
+                    } else {
+                        Spacer(
+                            Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+                                .testTag(nullTestTag)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(nullTestTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyListOnlyVisibleItemsAdded() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContentWithTestViewConfiguration {
+            Box(Modifier.mainAxisSize(200.dp)) {
+                LazyColumnOrRow(pivotOffsets = PivotOffsets(parentFraction = 0.4f)) {
+                    items(items) {
+                        Spacer(
+                            Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis()).testTag(it)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyListScrollToShowItems123() {
+        val items = (1..4).map { it.toString() }
+        rule.setContentWithTestViewConfiguration {
+            Box(Modifier.mainAxisSize(200.dp)) {
+                LazyColumnOrRow(
+                    modifier = Modifier.testTag(LazyListTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+                ) {
+                    items(items) {
+                        Box(
+                            Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+                                .testTag(it).focusable().border(3.dp, Color.Red)
+                        ) {
+                            BasicText(it)
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertIsNotDisplayed()
+    }
+
+    @Test
+    fun lazyListScrollToHideFirstItem() {
+        val items = (1..4).map { it.toString() }
+        rule.setContentWithTestViewConfiguration {
+            Box(Modifier.mainAxisSize(200.dp)) {
+                LazyColumnOrRow(modifier = Modifier.testTag(LazyListTag)) {
+                    items(items) {
+                        Box(
+                            Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+                                .testTag(it).focusable()
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyListScrollToShowItems234() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContentWithTestViewConfiguration {
+            Box(Modifier.mainAxisSize(200.dp)) {
+                LazyColumnOrRow(
+                    modifier = Modifier.testTag(LazyListTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+                ) {
+                    items(items) {
+                        Box(
+                            Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+                                .testTag(it).focusable()
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(4)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyListWrapsContent() = with(rule.density) {
+        val itemInsideLazyList = "itemInsideLazyList"
+        val itemOutsideLazyList = "itemOutsideLazyList"
+        var sameSizeItems by mutableStateOf(true)
+
+        rule.setContentWithTestViewConfiguration {
+            Column {
+                LazyColumnOrRow(Modifier.testTag(LazyListTag)) {
+                    items(listOf(1, 2)) {
+                        if (it == 1) {
+                            Spacer(Modifier.size(50.dp).testTag(itemInsideLazyList))
+                        } else {
+                            Spacer(Modifier.size(if (sameSizeItems) 50.dp else 70.dp))
+                        }
+                    }
+                }
+                Spacer(Modifier.size(50.dp).testTag(itemOutsideLazyList))
+            }
+        }
+
+        rule.onNodeWithTag(itemInsideLazyList)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyList)
+            .assertIsDisplayed()
+
+        var lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
+        var mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
+        var crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
+
+        assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+        assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+        assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+        assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(50.dp.roundToPx())
+
+        rule.runOnIdle {
+            sameSizeItems = false
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(itemInsideLazyList)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyList)
+            .assertIsDisplayed()
+
+        lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
+        mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
+        crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
+
+        assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+        assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(120.dp.roundToPx())
+        assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+        assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(70.dp.roundToPx())
+    }
+
+    @Test
+    fun compositionsAreDisposed_whenNodesAreScrolledOff() {
+        var composed: Boolean
+        var disposed = false
+        // Ten 31dp spacers in a 300dp list
+        val latch = CountDownLatch(10)
+
+        rule.setContentWithTestViewConfiguration {
+            // Fixed size to eliminate device size as a factor
+            Box(Modifier.testTag(LazyListTag).mainAxisSize(300.dp)) {
+                LazyColumnOrRow(Modifier.fillMaxSize()) {
+                    items(50) {
+                        DisposableEffect(NeverEqualObject) {
+                            composed = true
+                            // Signal when everything is done composing
+                            latch.countDown()
+                            onDispose {
+                                disposed = true
+                            }
+                        }
+
+                        // There will be 10 of these in the 300dp box
+                        Box(Modifier.mainAxisSize(31.dp).focusable()) {
+                            BasicText(it.toString())
+                        }
+                    }
+                }
+            }
+        }
+
+        latch.await()
+        composed = false
+
+        assertWithMessage("Compositions were disposed before we did any scrolling")
+            .that(disposed).isFalse()
+
+        // Mostly a validity check, this is not part of the behavior under test
+        assertWithMessage("Additional composition occurred for no apparent reason")
+            .that(composed).isFalse()
+
+        Thread.sleep(5000L)
+        rule.keyPress(
+            if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
+            13
+        )
+        Thread.sleep(5000L)
+
+        rule.waitForIdle()
+
+        assertWithMessage("No additional items were composed after scroll, scroll didn't work")
+            .that(composed).isTrue()
+
+        // We may need to modify this test once we prefetch/cache items outside the viewport
+        assertWithMessage(
+            "No compositions were disposed after scrolling, compositions were leaked"
+        ).that(disposed).isTrue()
+    }
+
+    @Test
+    fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
+        val thirdTag = "third"
+        val items = (1..3).toList()
+        var thirdHasSize by mutableStateOf(false)
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.fillMaxCrossAxis()
+                    .mainAxisSize(100.dp)
+                    .testTag(LazyListTag)
+            ) {
+                items(items) {
+                    if (it == 3) {
+                        Box(
+                            Modifier.testTag(thirdTag)
+                                .then(fillParentMaxCrossAxis())
+                                .mainAxisSize(if (thirdHasSize) 60.dp else 0.dp).focusable()
+                        )
+                    } else {
+                        Box(Modifier.then(fillParentMaxCrossAxis()).mainAxisSize(60.dp).focusable())
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag(thirdTag)
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            thirdHasSize = true
+        }
+
+        rule.waitForIdle()
+
+        rule.keyPress(2)
+
+        rule.onNodeWithTag(thirdTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun itemFillingParentWidth() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeight() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentSize() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidthFraction() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.fillParentMaxWidth(0.7f)
+                            .requiredHeight(50.dp)
+                            .testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(70.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeightFraction() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.requiredWidth(50.dp)
+                            .fillParentMaxHeight(0.3f)
+                            .testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(45.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeFraction() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(75.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeParentResized() {
+        var parentSize by mutableStateOf(100.dp)
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(parentSize)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            parentSize = 150.dp
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(150.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun whenNotAnymoreAvailableItemWasDisplayed() {
+        var items by mutableStateOf((1..30).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Box(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // after scroll we will display items 16-20
+        rule.keyPress(17)
+
+        rule.runOnIdle {
+            items = (1..10).toList()
+        }
+
+        // there is no item 16 anymore so we will just display the last items 6-10
+        rule.onNodeWithTag("6")
+            .assertStartPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenFewDisplayedItemsWereRemoved() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.keyPress(5)
+        rule.runOnIdle {
+            items = (1..8).toList()
+        }
+
+        // there are no more items 9 and 10, so we have to scroll back
+        rule.onNodeWithTag("4")
+            .assertStartPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenItemsBecameEmpty() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSizeIn(maxHeight = 100.dp, maxWidth = 100.dp)
+                    .testTag(LazyListTag)
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // after scroll we will display items 2-6
+        rule.keyPress(2)
+
+        rule.runOnIdle {
+            items = emptyList()
+        }
+
+        // there are no more items so the lazy list is zero sized
+        rule.onNodeWithTag(LazyListTag)
+            .assertWidthIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(0.dp)
+
+        // and has no children
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun scrollBackAndForth() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.keyPress(5)
+
+        // and scroll back
+        rule.keyPress(5, reverseScroll = true)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun tryToScrollBackwardWhenAlreadyOnTop() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Box(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // getting focus to the first element
+        rule.keyPress(2)
+        // we already displaying the first item, so this should do nothing
+        rule.keyPress(4, reverseScroll = true)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionIsAlmost(0.dp)
+        rule.onNodeWithTag("2")
+            .assertStartPositionIsAlmost(20.dp)
+        rule.onNodeWithTag("3")
+            .assertStartPositionIsAlmost(40.dp)
+        rule.onNodeWithTag("4")
+            .assertStartPositionIsAlmost(60.dp)
+        rule.onNodeWithTag("5")
+            .assertStartPositionIsAlmost(80.dp)
+    }
+
+    @Test
+    fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+        val items = listOf(NotStable(1), NotStable(2))
+        var firstItemRecomposed = 0
+        var secondItemRecomposed = 0
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    if (it.count == 1) {
+                        firstItemRecomposed++
+                    } else {
+                        secondItemRecomposed++
+                    }
+                    Box(Modifier.requiredSize(75.dp).focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+
+        rule.keyPress(2)
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun onlyOneMeasurePassForScrollEvent() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState()
+            state.prefetchingEnabled = false
+            LazyColumnOrRow(
+                Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        val initialMeasurePasses = state.numMeasurePasses
+
+        rule.runOnIdle {
+            with(rule.density) {
+                state.onScroll(-110.dp.toPx())
+            }
+        }
+
+        rule.waitForIdle()
+
+        assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+    }
+
+    @Test
+    fun onlyOneInitialMeasurePass() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(
+                Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.numMeasurePasses).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun scroll_makeListSmaller_scroll() {
+        var items by mutableStateOf((1..100).toList())
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Box(Modifier.requiredSize(10.dp).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(30)
+        rule.runOnIdle {
+            items = (1..11).toList()
+        }
+
+        rule.waitForIdle()
+        // try to scroll after the data set has been updated. this was causing a crash previously
+        rule.keyPress(1, reverseScroll = true)
+        rule.onNodeWithTag("11")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun initialScrollIsApplied() {
+        val items by mutableStateOf((0..20).toList())
+        lateinit var state: TvLazyListState
+        val expectedOffset = with(rule.density) { 10.dp.roundToPx() }
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState(2, expectedOffset)
+            LazyColumnOrRow(
+                Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo((-10).dp)
+    }
+
+    @Test
+    fun stateIsRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        var state: TvLazyListState? = null
+        restorationTester.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(
+                Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                state = state!!
+            ) {
+                items(20) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        val (index, scrollOffset) = rule.runOnIdle {
+            state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+        }
+
+        state = null
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+            assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+        }
+    }
+
+    @Test
+    fun snapToItemIndex() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState()
+            LazyColumnOrRow(
+                Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(20) {
+                    Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3, 10)
+            }
+            assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+        }
+    }
+
+    // TODO: Needs to be debugged and fixed for TV surfaces.
+    /*@Test
+    fun itemsAreNotRedrawnDuringScroll() {
+        val items = (0..20).toList()
+        val redrawCount = Array(6) { 0 }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag),
+                pivotOffsetConfig = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(items) {
+                    Box(
+                        Modifier.requiredSize(20.dp)
+                            .testTag(it.toString())
+                            .drawBehind {
+                                redrawCount[it]++
+                                if (redrawCount[it] != 1) {
+                                    Log.i("REMOVE_ME", Exception("Redrawn").stackTraceToString())
+                                }
+                            }
+                            .focusable()
+                    ) {
+                        BasicText(it.toString())
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(3)
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+        rule.runOnIdle {
+            redrawCount.forEachIndexed { index, i ->
+                assertWithMessage("Item with index $index was redrawn $i times")
+                    .that(i).isEqualTo(1)
+            }
+        }
+    }*/
+
+    @Test
+    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+        val redrawCount = Array(2) { 0 }
+        var stateUsedInDrawScope by mutableStateOf(false)
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+                items(2) {
+                    Spacer(
+                        Modifier.requiredSize(50.dp)
+                            .drawBehind {
+                                redrawCount[it]++
+                                if (it == 1) {
+                                    stateUsedInDrawScope.hashCode()
+                                }
+                            }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            stateUsedInDrawScope = true
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First items is not expected to be redrawn")
+                .that(redrawCount[0]).isEqualTo(1)
+            assertWithMessage("Second items is expected to be redrawn")
+                .that(redrawCount[1]).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSizeMinusOne).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(2) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.crossAxisSize(30.dp).mainAxisSize(itemSizeMinusOne)
+                        } else {
+                            Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertCrossAxisSizeIsEqualTo(20.dp)
+    }
+
+    @Test
+    fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+        val items = (0..2).toList()
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 1.75f).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(items) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.crossAxisSize(30.dp).mainAxisSize(itemSize / 2)
+                        } else if (it == 1) {
+                            Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize / 2)
+                        } else {
+                            Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertCrossAxisSizeIsEqualTo(30.dp)
+    }
+
+    @Test
+    fun usedWithArray() {
+        val items = arrayOf("1", "2", "3")
+
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow {
+                items(items) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun usedWithArrayIndexed() {
+        val items = arrayOf("1", "2", "3")
+
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow {
+                itemsIndexed(items) { index, item ->
+                    Spacer(Modifier.requiredSize(itemSize).testTag("$index*$item"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0*1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1*2")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2*3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun changeItemsCountAndScrollImmediately() {
+        lateinit var state: TvLazyListState
+        var count by mutableStateOf(100)
+        val composedIndexes = mutableListOf<Int>()
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(Modifier.fillMaxCrossAxis().mainAxisSize(10.dp), state) {
+                items(count) { index ->
+                    composedIndexes.add(index)
+                    Box(Modifier.size(20.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            composedIndexes.clear()
+            count = 10
+            runBlocking(AutoTestFrameClock()) {
+                state.scrollToItem(50)
+            }
+            composedIndexes.forEach {
+                assertThat(it).isLessThan(count)
+            }
+            assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+        }
+    }
+
+    @Test
+    fun overscrollingBackwardFromNotTheFirstPosition() {
+        val containerTag = "container"
+        val itemSizePx = 10
+        val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
+        val containerSize = itemSizeDp * 5
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier
+                    .testTag(containerTag)
+                    .size(containerSize)
+            ) {
+                LazyColumnOrRow(
+                    Modifier
+                        .testTag(LazyListTag)
+                        .background(Color.Blue),
+                    state = rememberLazyListState(2, 5)
+                ) {
+                    items(100) {
+                        Box(
+                            Modifier
+                                .fillMaxCrossAxis()
+                                .mainAxisSize(itemSizeDp)
+                                .testTag("$it")
+                                .focusable()
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(
+            if (vertical) NativeKeyEvent.KEYCODE_DPAD_UP else NativeKeyEvent.KEYCODE_DPAD_LEFT,
+            15
+        )
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertMainAxisSizeIsEqualTo(containerSize)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("4")
+            .assertStartPositionInRootIsEqualTo(containerSize - itemSizeDp)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun doesNotClipHorizontalOverdraw() {
+        rule.setContent {
+            Box(Modifier.size(60.dp).testTag("container").background(Color.Gray)) {
+                LazyColumnOrRow(
+                    Modifier
+                        .padding(20.dp)
+                        .fillMaxSize(),
+                    rememberLazyListState(1)
+                ) {
+                    items(4) {
+                        Box(Modifier.size(20.dp).drawOutsideOfBounds())
+                    }
+                }
+            }
+        }
+
+        val horizontalPadding = if (vertical) 0.dp else 20.dp
+        val verticalPadding = if (vertical) 20.dp else 0.dp
+
+        rule.onNodeWithTag("container")
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = Color.Red,
+                backgroundColor = Color.Gray,
+                horizontalPadding = horizontalPadding,
+                verticalPadding = verticalPadding
+            )
+    }
+
+    @Test
+    fun initialScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
+        lateinit var state: TvLazyListState
+        var itemsCount by mutableStateOf(0)
+        rule.setContent {
+            state = rememberLazyListState(2, 10)
+            LazyColumnOrRow(Modifier.fillMaxSize(), state) {
+                items(itemsCount) {
+                    Box(Modifier.size(20.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            itemsCount = 100
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun restoredScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
+        lateinit var state: TvLazyListState
+        var itemsCount = 100
+        val recomposeCounter = mutableStateOf(0)
+        val tester = StateRestorationTester(rule)
+        tester.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(Modifier.fillMaxSize(), state) {
+                recomposeCounter.value
+                items(itemsCount) {
+                    Box(Modifier.size(20.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2, 10)
+            }
+            itemsCount = 0
+        }
+
+        tester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            itemsCount = 100
+            recomposeCounter.value = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun animateScrollToItemDoesNotScrollPastItem() {
+        lateinit var state: TvLazyListState
+        var target = 0
+        var reverse = false
+        rule.setContent {
+            val listState = rememberLazyListState()
+            SideEffect {
+                state = listState
+            }
+            LazyColumnOrRow(Modifier.fillMaxSize(), listState) {
+                items(2500) { _ ->
+                    Box(Modifier.size(100.dp))
+                }
+            }
+
+            if (reverse) {
+                assertThat(listState.firstVisibleItemIndex).isAtLeast(target)
+            } else {
+                assertThat(listState.firstVisibleItemIndex).isAtMost(target)
+            }
+        }
+
+        // Try a bunch of different targets with varying spacing
+        listOf(500, 800, 1500, 1600, 1800).forEach {
+            target = it
+            rule.runOnIdle {
+                runBlocking(AutoTestFrameClock()) {
+                    state.animateScrollToItem(target)
+                }
+            }
+
+            rule.runOnIdle {
+                assertThat(state.firstVisibleItemIndex).isEqualTo(target)
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            }
+        }
+
+        reverse = true
+
+        listOf(1600, 1500, 800, 500, 0).forEach {
+            target = it
+            rule.runOnIdle {
+                runBlocking(AutoTestFrameClock()) {
+                    state.animateScrollToItem(target)
+                }
+            }
+
+            rule.runOnIdle {
+                assertThat(state.firstVisibleItemIndex).isEqualTo(target)
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            }
+        }
+    }
+
+    @Test
+    fun animateScrollToTheLastItemWhenItemsAreLargerThenTheScreen() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(Modifier.crossAxisSize(150.dp).mainAxisSize(100.dp), state) {
+                items(20) {
+                    Box(Modifier.size(150.dp))
+                }
+            }
+        }
+
+        // Try a bunch of different start indexes
+        listOf(0, 5, 12).forEach {
+            val startIndex = it
+            rule.runOnIdle {
+                runBlocking(AutoTestFrameClock()) {
+                    state.scrollToItem(startIndex)
+                    state.animateScrollToItem(19)
+                }
+            }
+
+            rule.runOnIdle {
+                assertThat(state.firstVisibleItemIndex).isEqualTo(19)
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            }
+        }
+    }
+
+    @Test
+    fun recreatingContentLambdaTriggersItemRecomposition() {
+        val countState = mutableStateOf(0)
+        rule.setContent {
+            val count = countState.value
+            LazyColumnOrRow {
+                item {
+                    BasicText(text = "Count $count")
+                }
+            }
+        }
+
+        rule.onNodeWithText("Count 0")
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            countState.value++
+        }
+
+        rule.onNodeWithText("Count 1")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun semanticsScroll_isAnimated() {
+        rule.mainClock.autoAdvance = false
+        val state = TvLazyListState()
+
+        rule.setContent {
+            LazyColumnOrRow(Modifier.testTag(LazyListTag), state = state) {
+                items(50) {
+                    Box(Modifier.mainAxisSize(200.dp))
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+
+        rule.onNodeWithTag(LazyListTag).performSemanticsAction(SemanticsActions.ScrollBy) {
+            if (vertical) {
+                it(0f, 100f)
+            } else {
+                it(100f, 0f)
+            }
+        }
+
+        // We haven't advanced time yet, make sure it's still zero
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+
+        // Advance and make sure we're partway through
+        // Note that we need two frames for the animation to actually happen
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        // The items are 200dp each, so still the first one, but offset
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+        assertThat(state.firstVisibleItemScrollOffset).isLessThan(100)
+
+        // Finish the scroll, make sure we're at the target
+        rule.mainClock.advanceTimeBy(5000)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(100)
+    }
+
+    @Test
+    fun maxIntElements() {
+        val itemSize = with(rule.density) { 15.toDp() }
+
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(itemSize * 3),
+                state = TvLazyListState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
+            ) {
+                items(Int.MAX_VALUE) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("${Int.MAX_VALUE - 3}").assertStartPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("${Int.MAX_VALUE - 2}").assertStartPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("${Int.MAX_VALUE - 1}").assertStartPositionInRootIsEqualTo(itemSize * 2)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
+        rule.onNodeWithTag("0").assertDoesNotExist()
+    }
+
+    @Test
+    fun scrollingByExactlyTheItemSize_switchesTheFirstVisibleItem() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+            ) {
+                items(5) {
+                    Spacer(
+                        Modifier.size(itemSize).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = true,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(3)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.keyPress(1)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .assert(keyNotDefined(SemanticsActions.ScrollBy))
+            .assert(keyNotDefined(SemanticsActions.ScrollToIndex))
+            // but we still have a read only scroll range property
+            .assert(
+                keyIsDefined(
+                    if (vertical) {
+                        SemanticsProperties.VerticalScrollAxisRange
+                    } else {
+                        SemanticsProperties.HorizontalScrollAxisRange
+                    }
+                )
+            )
+    }
+
+    @Test
+    fun withMissingItems() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumnOrRow(
+                modifier = Modifier.mainAxisSize(itemSize + 1.dp),
+                state = state
+            ) {
+                items(4) {
+                    if (it != 1) {
+                        Box(Modifier.size(itemSize).testTag(it.toString()).focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        rule.onNodeWithTag("2").assertIsDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(1)
+            }
+        }
+
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+        rule.onNodeWithTag("2").assertIsDisplayed()
+        rule.onNodeWithTag("3").assertIsDisplayed()
+    }
+
+    @Test
+    fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+        var remeasureCount = 0
+        val layoutModifier = Modifier.layout { measurable, constraints ->
+            remeasureCount++
+            val placeable = measurable.measure(constraints)
+            layout(placeable.width, placeable.height) {
+                placeable.place(0, 0)
+            }
+        }
+        val counter = mutableStateOf(0)
+
+        rule.setContentWithTestViewConfiguration {
+            counter.value // just to trigger recomposition
+            LazyColumnOrRow(
+                // this will return a new object everytime causing Lazy list recomposition
+                // without causing remeasure
+                Modifier.composed { layoutModifier }
+            ) {
+                items(1) {
+                    Spacer(Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasureCount).isEqualTo(1)
+            counter.value++
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasureCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun passingNegativeItemsCountIsNotAllowed() {
+        var exception: Exception? = null
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow {
+                try {
+                    items(-1) {
+                        Box(Modifier)
+                    }
+                } catch (e: Exception) {
+                    exception = e
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
+        }
+    }
+
+    @Test
+    fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+        var recomposeCount = 0
+        lateinit var state: TvLazyListState
+
+        rule.setContentWithTestViewConfiguration {
+            state = rememberLazyListState()
+            LazyColumnOrRow(
+                Modifier.composed {
+                    recomposeCount++
+                    Modifier
+                },
+                state
+            ) {
+                items(1000) {
+                    Spacer(Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(recomposeCount).isEqualTo(1)
+
+            runBlocking {
+                state.scrollToItem(100)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(recomposeCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun zIndexOnItemAffectsDrawingOrder() {
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.size(6.dp).testTag(LazyListTag)
+            ) {
+                items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
+                    Box(
+                        Modifier
+                            .mainAxisSize(2.dp)
+                            .crossAxisSize(6.dp)
+                            .zIndex(if (color == Color.Green) 1f else 0f)
+                            .drawBehind {
+                                drawRect(
+                                    color,
+                                    topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
+                                    size = Size(20.dp.toPx(), 20.dp.toPx())
+                                )
+                            })
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .captureToImage()
+            .assertPixels { Color.Green }
+    }
+
+    // ********************* END OF TESTS *********************
+    // Helper functions, etc. live below here
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+    }
+}
+
+internal val NeverEqualObject = object {
+    override fun equals(other: Any?): Boolean {
+        return false
+    }
+}
+
+private data class NotStable(val count: Int)
+
+internal const val TestTouchSlop = 18f
+
+internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
+    isEqualTo(expected, 1)
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+    isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun ComposeContentTestRule.setContentWithTestViewConfiguration(
+    composable: @Composable () -> Unit
+) {
+    this.setContent {
+        WithTouchSlop(TestTouchSlop, composable)
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
new file mode 100644
index 0000000..eccaaed
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListsContentPaddingTest(orientation: Orientation) :
+    BaseLazyListTestWithOrientation(orientation) {
+
+    private val LazyListTag = "LazyList"
+    private val ItemTag = "item"
+    private val ContainerTag = "container"
+
+    private var itemSize: Dp = Dp.Infinity
+    private var smallPaddingSize: Dp = Dp.Infinity
+    private var itemSizePx = 50f
+    private var smallPaddingSizePx = 12f
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = itemSizePx.toDp()
+            smallPaddingSize = smallPaddingSizePx.toDp()
+        }
+    }
+
+    @Test
+    fun contentPaddingIsApplied() {
+        lateinit var state: TvLazyListState
+        val containerSize = itemSize * 2
+        val largePaddingSize = itemSize
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(containerSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(
+                    mainAxis = largePaddingSize,
+                    crossAxis = smallPaddingSize
+                )
+            ) {
+                items(listOf(1)) {
+                    Spacer(
+                        Modifier
+                            .then(fillParentMaxCrossAxis())
+                            .mainAxisSize(itemSize)
+                            .testTag(ItemTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertCrossAxisStartPositionInRootIsEqualTo(smallPaddingSize)
+            .assertStartPositionInRootIsEqualTo(largePaddingSize)
+            .assertCrossAxisSizeIsEqualTo(containerSize - smallPaddingSize * 2)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        state.scrollBy(largePaddingSize)
+
+        rule.onNodeWithTag(ItemTag)
+            .assertStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun contentPaddingIsNotAffectingScrollPosition() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(itemSize * 2)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(mainAxis = itemSize)
+            ) {
+                items(listOf(1)) {
+                    Spacer(
+                        Modifier
+                            .then(fillParentMaxCrossAxis())
+                            .mainAxisSize(itemSize)
+                            .testTag(ItemTag))
+                }
+            }
+        }
+
+        state.assertScrollPosition(0, 0.dp)
+
+        state.scrollBy(itemSize)
+
+        state.assertScrollPosition(0, itemSize)
+    }
+
+    @Test
+    fun scrollForwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: TvLazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(mainAxis = padding)
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(padding)
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize + padding)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+        state.scrollBy(padding)
+
+        state.assertScrollPosition(1, padding - itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2)
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun scrollBackwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: TvLazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(itemSize + padding * 2)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(mainAxis = padding)
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize * 1.5f)
+
+        state.assertScrollPosition(1, itemSize * 0.5f)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+    }
+
+    @Test
+    fun scrollForwardTillTheEnd() {
+        lateinit var state: TvLazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(mainAxis = padding)
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+        // there are no space to scroll anymore, so it should change nothing
+        state.scrollBy(10.dp)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
+    }
+
+    @Test
+    fun scrollForwardTillTheEndAndABitBack() {
+        lateinit var state: TvLazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.requiredSize(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
+                contentPadding = PaddingValues(mainAxis = padding)
+            ) {
+                items((0..3).toList()) {
+                    Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize / 2)
+
+        state.assertScrollPosition(2, itemSize / 2)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    }
+
+    @Test
+    fun contentPaddingAndWrapContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyColumnOrRow(
+                    contentPadding = PaddingValues(
+                        beforeContentCrossAxis = 2.dp,
+                        beforeContent = 4.dp,
+                        afterContentCrossAxis = 6.dp,
+                        afterContent = 8.dp
+                    )
+                ) {
+                    items(listOf(1)) {
+                        Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertCrossAxisStartPositionInRootIsEqualTo(2.dp)
+            .assertStartPositionInRootIsEqualTo(4.dp)
+            .assertCrossAxisSizeIsEqualTo(itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(itemSize + 2.dp + 6.dp)
+            .assertMainAxisSizeIsEqualTo(itemSize + 4.dp + 8.dp)
+    }
+
+    @Test
+    fun contentPaddingAndNoContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyColumnOrRow(
+                    contentPadding = PaddingValues(
+                        beforeContentCrossAxis = 2.dp,
+                        beforeContent = 4.dp,
+                        afterContentCrossAxis = 6.dp,
+                        afterContent = 8.dp
+                    )
+                ) { }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(8.dp)
+            .assertMainAxisSizeIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun contentPaddingAndZeroSizedItem() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyColumnOrRow(
+                    contentPadding = PaddingValues(
+                        beforeContentCrossAxis = 2.dp,
+                        beforeContent = 4.dp,
+                        afterContentCrossAxis = 6.dp,
+                        afterContent = 8.dp
+                    )
+                ) {
+                    items(0) { }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisSizeIsEqualTo(8.dp)
+            .assertMainAxisSizeIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun contentPaddingAndReverseLayout() {
+        val topPadding = itemSize * 2
+        val bottomPadding = itemSize / 2
+        val listSize = itemSize * 3
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(listSize),
+                contentPadding = PaddingValues(
+                    beforeContent = topPadding,
+                    afterContent = bottomPadding
+                ),
+            ) {
+                items(3) { index ->
+                    Box(Modifier.requiredSize(itemSize).testTag("$index"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+        // Partially visible.
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(-itemSize / 2)
+
+        // Scroll to the top.
+        state.scrollBy(itemSize * 2.5f)
+
+        rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(topPadding)
+        // Shouldn't be visible
+        rule.onNodeWithTag("1").assertIsNotDisplayed()
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+    }
+
+    @Test
+    fun overscrollWithContentPadding() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = smallPaddingSize)
+                ) {
+                    items(2) {
+                        Box(Modifier.testTag("$it").fillParentMaxSize())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(smallPaddingSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            runBlocking {
+                // itemSizePx is the maximum offset, plus if we overscroll the content padding
+                // the layout mechanism will decide the item 0 is not needed until we start
+                // filling the over scrolled gap.
+                state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(smallPaddingSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+            .assertMainAxisSizeIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_initialState() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(1, 0.dp)
+            state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+        // the whole end content padding is displayed
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 4.5f)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertStartPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 1.5f)
+            state.assertVisibleItems(3 to -itemSize * 1.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_initialState() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 2)
+
+        rule.onNodeWithTag("0")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(2, 0.dp)
+            state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+        // only the end content padding is displayed
+        lateinit var state: TvLazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyColumnOrRow(
+                    state = state,
+                    contentPadding = PaddingValues(mainAxis = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(
+            itemSize * 1.5f + // container size
+                itemSize * 2 + // start padding
+                itemSize * 3 // all items
+        )
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 3.5f)
+            state.assertVisibleItems(3 to -itemSize * 3.5f)
+        }
+    }
+
+    private fun TvLazyListState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+        assertThat(firstVisibleItemIndex).isEqualTo(index)
+        assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+    }
+
+    private fun TvLazyListState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+        assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+            .isEqualTo(from.roundToPx() to to.roundToPx())
+    }
+
+    private fun TvLazyListState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+        with(rule.density) {
+            assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset })
+                .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+        }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt
new file mode 100644
index 0000000..a868e08
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListsIndexedTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun lazyColumnShowsIndexedItems() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            TvLazyColumn(
+                Modifier.height(200.dp),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.height(101.dp).fillParentMaxWidth()
+                            .testTag("$index-$item").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun columnWithIndexesComposedWithCorrectIndexAndItem() {
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            TvLazyColumn(
+                Modifier.height(200.dp),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    BasicText(
+                        "${index}x$item", Modifier.fillParentMaxWidth().requiredHeight(100.dp)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithText("0x0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertTopPositionInRootIsEqualTo(100.dp)
+    }
+
+    @Test
+    fun lazyRowShowsIndexedItems() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            TvLazyRow(
+                Modifier.width(200.dp),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.width(101.dp).fillParentMaxHeight()
+                            .testTag("$index-$item").focusable()
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            TvLazyRow(
+                Modifier.width(200.dp),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    BasicText(
+                        "${index}x$item",
+                        Modifier.fillParentMaxHeight().requiredWidth(100.dp).focusable()
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithText("0x0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertLeftPositionInRootIsEqualTo(100.dp)
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
new file mode 100644
index 0000000..1798212
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListsReverseLayoutTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+    }
+
+    @Test
+    fun column_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                    Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_emitTwoItems_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                }
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_initialScrollPositionIs0() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun column_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_scrollForwardHalfWay() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 3)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    @Test
+    fun column_scrollForwardTillTheEnd() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // we scroll a bit more than it is possible just to make sure we would stop correctly
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 6)
+
+        rule.runOnIdle {
+            with(rule.density) {
+                val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+                    itemSize * state.firstVisibleItemIndex
+                assertThat(realOffset).isEqualTo(itemSize * 2)
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                    Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_emitTwoItems_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                }
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("1"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_initialScrollPositionIs0() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun row_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_scrollForwardHalfWay() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    @Test
+    fun row_scrollForwardTillTheEnd() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                }
+            }
+        }
+
+        // we scroll a bit more than it is possible just to make sure we would stop correctly
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 6)
+        rule.runOnIdle {
+            with(rule.density) {
+                val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+                    itemSize * state.firstVisibleItemIndex
+                assertThat(realOffset).isEqualTo(itemSize * 2)
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                TvLazyRow(
+                    reverseLayout = true,
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    item {
+                        Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                        Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun row_rtl_emitTwoItems_positionedReversed() {
+        rule.setContentWithTestViewConfiguration {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                TvLazyRow(
+                    reverseLayout = true,
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    item {
+                        Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                    }
+                    item {
+                        Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun row_rtl_scrollForwardHalfWay() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                TvLazyRow(
+                    reverseLayout = true,
+                    state = rememberLazyListState().also { state = it },
+                    modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+                ) {
+                    items((0..2).toList()) {
+                        Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(-scrolled)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+    }
+
+    @Test
+    fun column_whenParameterChanges() {
+        var reverse by mutableStateOf(true)
+        rule.setContentWithTestViewConfiguration {
+            TvLazyColumn(
+                reverseLayout = reverse,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                    Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            reverse = false
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_whenParameterChanges() {
+        var reverse by mutableStateOf(true)
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                reverseLayout = reverse,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                item {
+                    Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+                    Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            reverse = false
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
new file mode 100644
index 0000000..c79c0f8
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyNestedScrollingTest {
+    private val LazyTag = "LazyTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val expectedDragOffset = 20f
+    private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
+
+    @Test
+    fun column_nestedScrollingBackwardInitially() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyColumn(
+                    Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(100f)
+        }
+    }
+
+    @Test
+    fun column_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyColumn(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll forward
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+        // scroll back so we again on 0 position
+        // we scroll one extra dp to prevent rounding issues
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun column_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+        val items = (1..2).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyColumn(
+                    Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Box(Modifier.requiredSize(40.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun column_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    state = scrollable
+                )
+            ) {
+                TvLazyColumn(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll till the end
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun row_nestedScrollingBackwardInitially() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    state = scrollable
+                )
+            ) {
+                TvLazyRow(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    state = scrollable
+                )
+            ) {
+                TvLazyRow(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll forward
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
+
+        // scroll back so we again on 0 position
+        // we scroll one extra dp to prevent rounding issues
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 2)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+        val items = (1..2).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    state = scrollable
+                )
+            ) {
+                TvLazyRow(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(40.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+
+    @Test
+    fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+        val items = (1..3).toList()
+        var draggedOffset = 0f
+        val scrollable = ScrollableState {
+            draggedOffset += it
+            it
+        }
+        rule.setContentWithTestViewConfiguration {
+            Box(
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    state = scrollable
+                )
+            ) {
+                TvLazyRow(
+                    modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+                    pivotOffsets = PivotOffsets(parentFraction = 0f)
+                ) {
+                    items(items) {
+                        Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+                    }
+                }
+            }
+        }
+
+        // scroll till the end
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+        rule.onNodeWithTag(LazyTag)
+            .performTouchInput {
+                draggedOffset = 0f
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+                up()
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt
new file mode 100644
index 0000000..13bfd51
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyRowTest {
+    private val LazyListTag = "LazyListTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val firstItemTag = "firstItemTag"
+    private val secondItemTag = "secondItemTag"
+
+    private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
+        rule.setContentWithTestViewConfiguration {
+            TvLazyRow(
+                Modifier.testTag(LazyListTag).requiredHeight(100.dp),
+                verticalAlignment = verticalGravity,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(listOf(1, 2)) {
+                    if (it == 1) {
+                        Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
+                    } else {
+                        Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertIsDisplayed()
+
+        val lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        with(rule.density) {
+            // Verify the height of the row
+            assertThat(lazyRowBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+            assertThat(lazyRowBounds.bottom.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+        }
+    }
+
+    @Test
+    fun lazyRowAlignmentCenterVertically() {
+        prepareLazyRowForAlignment(Alignment.CenterVertically)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 25.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 15.dp)
+    }
+
+    @Test
+    fun lazyRowAlignmentTop() {
+        prepareLazyRowForAlignment(Alignment.Top)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+    }
+
+    @Test
+    fun lazyRowAlignmentBottom() {
+        prepareLazyRowForAlignment(Alignment.Bottom)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 30.dp)
+    }
+
+    @Test
+    fun scrollsLeftInRtl() {
+        lateinit var state: TvLazyListState
+        rule.setContentWithTestViewConfiguration {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Box(Modifier.width(100.dp)) {
+                    state = rememberLazyListState()
+                    TvLazyRow(
+                        Modifier.testTag(LazyListTag),
+                        state,
+                        pivotOffsets =
+                        PivotOffsets(parentFraction = 0f)
+                    ) {
+                        items(4) {
+                            Box(
+                                Modifier.width(101.dp).fillParentMaxHeight().testTag("$it")
+                                    .focusable()
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
new file mode 100644
index 0000000..53c9775
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import android.R.id.accessibilityActionScrollDown
+import android.R.id.accessibilityActionScrollLeft
+import android.R.id.accessibilityActionScrollRight
+import android.R.id.accessibilityActionScrollUp
+import android.view.View
+import android.view.accessibility.AccessibilityNodeProvider
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollAccessibilityTest(private val config: TestConfig) {
+    data class TestConfig(
+        val horizontal: Boolean,
+        val rtl: Boolean,
+        val reversed: Boolean
+    ) {
+        val vertical = !horizontal
+
+        override fun toString(): String {
+            return (if (horizontal) "horizontal" else "vertical") +
+                (if (rtl) ",rtl" else ",ltr") +
+                (if (reversed) ",reversed" else "")
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() =
+            listOf(true, false).flatMap { horizontal ->
+                listOf(false, true).flatMap { rtl ->
+                    listOf(false, true).map { reversed ->
+                        TestConfig(horizontal, rtl, reversed)
+                    }
+                }
+            }
+    }
+
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    private val scrollerTag = "ScrollerTest"
+    private var composeView: View? = null
+    private val accessibilityNodeProvider: AccessibilityNodeProvider
+        get() = checkNotNull(composeView) {
+            "composeView not initialized. Did `composeView = LocalView.current` not work?"
+        }.let { composeView ->
+            ViewCompat
+                .getAccessibilityDelegate(composeView)!!
+                .getAccessibilityNodeProvider(composeView)!!
+                .provider as AccessibilityNodeProvider
+        }
+
+    @Test
+    fun scrollForward() {
+        testRelativeDirection(58, ACTION_SCROLL_FORWARD)
+    }
+
+    @Test
+    fun scrollBackward() {
+        testRelativeDirection(41, ACTION_SCROLL_BACKWARD)
+    }
+
+    @Test
+    fun scrollRight() {
+        testAbsoluteDirection(58, accessibilityActionScrollRight, config.horizontal)
+    }
+
+    @Test
+    fun scrollLeft() {
+        testAbsoluteDirection(41, accessibilityActionScrollLeft, config.horizontal)
+    }
+
+    @Test
+    fun scrollDown() {
+        testAbsoluteDirection(58, accessibilityActionScrollDown, config.vertical)
+    }
+
+    @Test
+    fun scrollUp() {
+        testAbsoluteDirection(41, accessibilityActionScrollUp, config.vertical)
+    }
+
+    @Test
+    fun verifyScrollActionsAtStart() {
+        createScrollableContent_StartAtStart()
+        verifyNodeInfoScrollActions(
+            expectForward = !config.reversed,
+            expectBackward = config.reversed
+        )
+    }
+
+    @Test
+    fun verifyScrollActionsInMiddle() {
+        createScrollableContent_StartInMiddle()
+        verifyNodeInfoScrollActions(
+            expectForward = true,
+            expectBackward = true
+        )
+    }
+
+    @Test
+    fun verifyScrollActionsAtEnd() {
+        createScrollableContent_StartAtEnd()
+        verifyNodeInfoScrollActions(
+            expectForward = config.reversed,
+            expectBackward = !config.reversed
+        )
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+     * has been reached. The canonical target is the item that we expect to see when moving
+     * forward in a non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR).
+     * The actual target is either the canonical target or the target that is as far from the
+     * middle of the lazy list as the canonical target, but on the other side of the middle,
+     * depending on the [configuration][config].
+     */
+    private fun testRelativeDirection(canonicalTarget: Int, accessibilityAction: Int) {
+        val target = if (!config.reversed) canonicalTarget else 100 - canonicalTarget - 1
+        testScrollAction(target, accessibilityAction)
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+     * has been reached (but only if we [expect][expectActionSuccess] the action to succeed).
+     * The canonical target is the item that we expect to see when moving forward in a
+     * non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual
+     * target is either the canonical target or the target that is as far from the middle of the
+     * scrollable as the canonical target, but on the other side of the middle, depending on the
+     * [configuration][config].
+     */
+    private fun testAbsoluteDirection(
+        canonicalTarget: Int,
+        accessibilityAction: Int,
+        expectActionSuccess: Boolean
+    ) {
+        var target = canonicalTarget
+        if (config.horizontal && config.rtl) {
+            target = 100 - target - 1
+        }
+        if (config.reversed) {
+            target = 100 - target - 1
+        }
+        testScrollAction(target, accessibilityAction, expectActionSuccess)
+    }
+
+    /**
+     * Setup the test, run the given [accessibilityAction], and check if the [target] has been
+     * reached (but only if we [expect][expectActionSuccess] the action to succeed).
+     */
+    private fun testScrollAction(
+        target: Int,
+        accessibilityAction: Int,
+        expectActionSuccess: Boolean = true
+    ) {
+        createScrollableContent_StartInMiddle()
+        rule.onNodeWithText("$target").assertDoesNotExist()
+
+        val returnValue = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+            accessibilityNodeProvider.performAction(id, accessibilityAction, null)
+        }
+
+        assertThat(returnValue).isEqualTo(expectActionSuccess)
+        if (expectActionSuccess) {
+            rule.onNodeWithText("$target").assertIsDisplayed()
+        } else {
+            rule.onNodeWithText("$target").assertDoesNotExist()
+        }
+    }
+
+    /**
+     * Checks if all of the scroll actions are present or not according to what we expect based on
+     * [expectForward] and [expectBackward]. The scroll actions that are checked are forward,
+     * backward, left, right, up and down. The expectation parameters must already account for
+     * [reversing][TestConfig.reversed].
+     */
+    private fun verifyNodeInfoScrollActions(expectForward: Boolean, expectBackward: Boolean) {
+        val nodeInfo = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+            rule.runOnUiThread {
+                accessibilityNodeProvider.createAccessibilityNodeInfo(id)
+            }
+        }
+
+        val actions = nodeInfo.actionList.map { it.id }
+
+        assertThat(actions).contains(expectForward, ACTION_SCROLL_FORWARD)
+        assertThat(actions).contains(expectBackward, ACTION_SCROLL_BACKWARD)
+
+        if (config.horizontal) {
+            val expectLeft = if (config.rtl) expectForward else expectBackward
+            val expectRight = if (config.rtl) expectBackward else expectForward
+            assertThat(actions).contains(expectLeft, accessibilityActionScrollLeft)
+            assertThat(actions).contains(expectRight, accessibilityActionScrollRight)
+            assertThat(actions).contains(false, accessibilityActionScrollDown)
+            assertThat(actions).contains(false, accessibilityActionScrollUp)
+        } else {
+            assertThat(actions).contains(false, accessibilityActionScrollLeft)
+            assertThat(actions).contains(false, accessibilityActionScrollRight)
+            assertThat(actions).contains(expectForward, accessibilityActionScrollDown)
+            assertThat(actions).contains(expectBackward, accessibilityActionScrollUp)
+        }
+    }
+
+    private fun IterableSubject.contains(expectPresent: Boolean, element: Any) {
+        if (expectPresent) {
+            contains(element)
+        } else {
+            doesNotContain(element)
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts at the first item, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartAtStart() {
+        createScrollableContent {
+            // Start at the start:
+            // -> pretty basic
+            rememberLazyListState(0, 0)
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts in the middle, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartInMiddle() {
+        createScrollableContent {
+            // Start at the middle:
+            // Content size: 100 items * 21dp per item = 2100dp
+            // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+            // Content outside viewport: 2100dp - 100dp = 2000dp
+            // -> centered when 1000dp on either side, which is 47 items + 13dp
+            rememberLazyListState(
+                47,
+                with(LocalDensity.current) { 13.dp.roundToPx() }
+            )
+        }
+    }
+
+    /**
+     * Creates a Row/Column that starts at the last item, according to [createScrollableContent]
+     */
+    private fun createScrollableContent_StartAtEnd() {
+        createScrollableContent {
+            // Start at the end:
+            // Content size: 100 items * 21dp per item = 2100dp
+            // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+            // Content outside viewport: 2100dp - 100dp = 2000dp
+            // -> at the end when offset at 2000dp, which is 95 items + 5dp
+            rememberLazyListState(
+                95,
+                with(LocalDensity.current) { 5.dp.roundToPx() }
+            )
+        }
+    }
+
+    /**
+     * Creates a Row/Column with a viewport of 100.dp, containing 100 items each 17.dp in size.
+     * The items have a text with their index (ASC), and where the viewport starts is determined
+     * by the given [lambda][rememberLazyListState]. All properties from [config] are applied.
+     * The viewport has padding around it to make sure scroll distance doesn't include padding.
+     */
+    private fun createScrollableContent(rememberLazyListState: @Composable () -> TvLazyListState) {
+        rule.setContent {
+            composeView = LocalView.current
+            val lazyContent: TvLazyListScope.() -> Unit = {
+                items(100) {
+                    Box(Modifier.requiredSize(21.dp).background(Color.Yellow)) {
+                        BasicText("$it", Modifier.align(Alignment.Center))
+                    }
+                }
+            }
+
+            val state = rememberLazyListState()
+
+            Box(Modifier.requiredSize(200.dp).background(Color.White)) {
+                val direction = if (config.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
+                CompositionLocalProvider(LocalLayoutDirection provides direction) {
+                    if (config.horizontal) {
+                        TvLazyRow(
+                            Modifier.testTag(scrollerTag).matchParentSize(),
+                            state = state,
+                            contentPadding = PaddingValues(50.dp),
+                            reverseLayout = config.reversed,
+                            verticalAlignment = Alignment.CenterVertically,
+                            pivotOffsets =
+                            PivotOffsets(parentFraction = 0f)
+                        ) {
+                            lazyContent()
+                        }
+                    } else {
+                        TvLazyColumn(
+                            Modifier.testTag(scrollerTag).matchParentSize(),
+                            state = state,
+                            contentPadding = PaddingValues(50.dp),
+                            reverseLayout = config.reversed,
+                            horizontalAlignment = Alignment.CenterHorizontally,
+                            pivotOffsets =
+                            PivotOffsets(parentFraction = 0f)
+                        ) {
+                            lazyContent()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun <T> SemanticsNodeInteraction.withSemanticsNode(block: SemanticsNode.() -> T): T {
+        return block.invoke(fetchSemanticsNode())
+    }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt
new file mode 100644
index 0000000..e8416f6
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.FloatSpringSpec
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollTest(private val orientation: Orientation) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val vertical: Boolean
+        get() = orientation == Orientation.Vertical
+
+    private val itemsCount = 20
+    private lateinit var state: TvLazyListState
+
+    private val itemSizePx = 100
+    private var itemSizeDp = Dp.Unspecified
+    private var containerSizeDp = Dp.Unspecified
+
+    lateinit var scope: CoroutineScope
+
+    @Before
+    fun setup() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            containerSizeDp = itemSizeDp * 3
+        }
+        rule.setContent {
+            state = rememberLazyListState()
+            scope = rememberCoroutineScope()
+            TestContent()
+        }
+    }
+
+    @Test
+    fun setupWorks() {
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun scrollToItem() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun scrollToItemWithOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(3, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun scrollToItemWithNegativeOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(3, -10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+        val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
+        assertThat(item3Offset).isEqualTo(10)
+    }
+
+    @Test
+    fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount - 3, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+    }
+
+    @Test
+    fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(1, -(itemSizePx + 10))
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+    }
+
+    @Test
+    fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount + 2)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+    }
+
+    @Test
+    fun animateScrollBy() = runBlocking {
+        val scrollDistance = 320
+
+        val expectedIndex = scrollDistance / itemSizePx // resolves to 3
+        val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
+
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollBy(scrollDistance.toFloat())
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(expectedIndex)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+    }
+
+    @Test
+    fun animateScrollToItem() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(5, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(3, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithNegativeOffset() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(3, -10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+        val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
+        assertThat(item3Offset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(itemsCount - 3, 10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+    }
+
+    @Test
+    fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(1, -(itemSizePx + 10))
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+    }
+
+    @Test
+    fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(itemsCount + 2)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+    }
+
+    @Test
+    fun animatePerFrameForwardToVisibleItem() {
+        assertSpringAnimation(toIndex = 2)
+    }
+
+    @Test
+    fun animatePerFrameForwardToVisibleItemWithOffset() {
+        assertSpringAnimation(toIndex = 2, toOffset = 35)
+    }
+
+    @Test
+    fun animatePerFrameForwardToNotVisibleItem() {
+        assertSpringAnimation(toIndex = 8)
+    }
+
+    @Test
+    fun animatePerFrameForwardToNotVisibleItemWithOffset() {
+        assertSpringAnimation(toIndex = 10, toOffset = 35)
+    }
+
+    @Test
+    fun animatePerFrameBackward() {
+        assertSpringAnimation(toIndex = 1, fromIndex = 6)
+    }
+
+    @Test
+    fun animatePerFrameBackwardWithOffset() {
+        assertSpringAnimation(toIndex = 1, fromIndex = 5, fromOffset = 58)
+    }
+
+    @Test
+    fun animatePerFrameBackwardWithInitialOffset() {
+        assertSpringAnimation(toIndex = 0, toOffset = 20, fromIndex = 8)
+    }
+
+    private fun assertSpringAnimation(
+        toIndex: Int,
+        toOffset: Int = 0,
+        fromIndex: Int = 0,
+        fromOffset: Int = 0
+    ) {
+        if (fromIndex != 0 || fromOffset != 0) {
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollToItem(fromIndex, fromOffset)
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
+
+        rule.mainClock.autoAdvance = false
+
+        scope.launch {
+            state.animateScrollToItem(toIndex, toOffset)
+        }
+
+        while (!state.isScrollInProgress) {
+            Thread.sleep(5)
+        }
+
+        val startOffset = (fromIndex * itemSizePx + fromOffset).toFloat()
+        val endOffset = (toIndex * itemSizePx + toOffset).toFloat()
+        val spec = FloatSpringSpec()
+
+        val duration =
+            TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
+        rule.mainClock.advanceTimeByFrame()
+        var expectedTime = rule.mainClock.currentTime
+        for (i in 0..duration step FrameDuration) {
+            val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
+            val expectedValue =
+                spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
+            val actualValue =
+                (state.firstVisibleItemIndex * itemSizePx + state.firstVisibleItemScrollOffset)
+            assertWithMessage(
+                "On animation frame at $i index=${state.firstVisibleItemIndex} " +
+                    "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
+            ).that(actualValue).isEqualTo(expectedValue.roundToInt(), tolerance = 1)
+
+            rule.mainClock.advanceTimeBy(FrameDuration)
+            expectedTime += FrameDuration
+            assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+            rule.waitForIdle()
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
+    }
+
+    @Composable
+    private fun TestContent() {
+        if (vertical) {
+            TvLazyColumn(
+                Modifier.height(containerSizeDp),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(itemsCount) {
+                    ItemContent()
+                }
+            }
+        } else {
+            TvLazyRow(
+                Modifier.width(containerSizeDp),
+                state,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(itemsCount) {
+                    ItemContent()
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun ItemContent() {
+        val modifier = if (vertical) {
+            Modifier.height(itemSizeDp)
+        } else {
+            Modifier.width(itemSizeDp)
+        }
+        Spacer(modifier)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+    }
+}
+
+private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt
new file mode 100644
index 0000000..2ac1492
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
+import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests the semantics properties defined on a LazyList:
+ * - GetIndexForKey
+ * - ScrollToIndex
+ *
+ * GetIndexForKey:
+ * Create a lazy list, iterate over all indices, verify key of each of them
+ *
+ * ScrollToIndex:
+ * Create a lazy list, scroll to an item off screen, verify shown items
+ *
+ * All tests performed in [runTest], scenarios set up in the test methods.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazySemanticsTest {
+    private val N = 20
+    private val LazyListTag = "lazy_list"
+    private val LazyListModifier = Modifier.testTag(LazyListTag).requiredSize(100.dp)
+
+    private fun tag(index: Int): String = "tag_$index"
+    private fun key(index: Int): String = "key_$index"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun itemSemantics_column() {
+        rule.setContent {
+            TvLazyColumn(
+                LazyListModifier,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                repeat(N) {
+                    item(key = key(it)) {
+                        SpacerInColumn(it)
+                    }
+                }
+            }
+        }
+        runTest()
+    }
+
+    @Test
+    fun itemsSemantics_column() {
+        rule.setContent {
+            TvLazyColumn(
+                LazyListModifier,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(items = List(N) { it }, key = { key(it) }) {
+                    SpacerInColumn(it)
+                }
+            }
+        }
+        runTest()
+    }
+
+    @Test
+    fun itemSemantics_row() {
+        rule.setContent {
+            TvLazyRow(
+                LazyListModifier,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                repeat(N) {
+                    item(key = key(it)) {
+                        SpacerInRow(it)
+                    }
+                }
+            }
+        }
+        runTest()
+    }
+
+    @Test
+    fun itemsSemantics_row() {
+        rule.setContent {
+            TvLazyRow(
+                LazyListModifier,
+                pivotOffsets = PivotOffsets(parentFraction = 0f)
+            ) {
+                items(items = List(N) { it }, key = { key(it) }) {
+                    SpacerInRow(it)
+                }
+            }
+        }
+        runTest()
+    }
+
+    private fun runTest() {
+        checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
+
+        // Verify IndexForKey
+        rule.onNodeWithTag(LazyListTag).assert(
+            SemanticsMatcher.keyIsDefined(IndexForKey).and(
+                SemanticsMatcher("keys match") { node ->
+                    val actualIndex = node.config.getOrNull(IndexForKey)!!
+                    (0 until N).all { expectedIndex ->
+                        expectedIndex == actualIndex.invoke(key(expectedIndex))
+                    }
+                }
+            )
+        )
+
+        // Verify ScrollToIndex
+        rule.onNodeWithTag(LazyListTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
+
+        invokeScrollToIndex(targetIndex = 10)
+        checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
+
+        invokeScrollToIndex(targetIndex = N - 1)
+        checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
+    }
+
+    private fun invokeScrollToIndex(targetIndex: Int) {
+        val node = rule.onNodeWithTag(LazyListTag)
+            .fetchSemanticsNode("Failed: invoke ScrollToIndex")
+        rule.runOnUiThread {
+            node.config[ScrollToIndex].action!!.invoke(targetIndex)
+        }
+    }
+
+    private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
+        if (firstExpectedItem > 0) {
+            rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
+        }
+        (firstExpectedItem..lastExpectedItem).forEach {
+            rule.onNodeWithTag(tag(it)).assertExists()
+        }
+        if (firstExpectedItem < N - 1) {
+            rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
+        }
+    }
+
+    @Composable
+    private fun SpacerInColumn(index: Int) {
+        Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
+    }
+
+    @Composable
+    private fun SpacerInRow(index: Int) {
+        Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt
new file mode 100644
index 0000000..00904a5
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusGroup
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.Orientation.Horizontal
+import androidx.compose.foundation.gestures.Orientation.Vertical
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.onFocusedBoundsChanged
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.OnPlacedModifier
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/* Copied from
+ compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/
+ Scrollable.kt and modified */
+
+/**
+ * Configure touch scrolling and flinging for the UI element in a single [Orientation].
+ *
+ * Users should update their state themselves using default [ScrollableState] and its
+ * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect
+ * their own state in UI when using this component.
+ *
+ * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be
+ * interpreted by the user land logic and contains useful information about on-going events.
+ * @param orientation orientation of the scrolling
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param enabled whether or not scrolling in enabled
+ * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
+ * behave like bottom to top and left to right will behave like right to left.
+ * drag events when this scrollable is being dragged.
+ */
+
+@OptIn(ExperimentalFoundationApi::class)
+fun Modifier.marioScrollable(
+    state: ScrollableState,
+    orientation: Orientation,
+    pivotOffsets: PivotOffsets,
+    enabled: Boolean = true,
+    reverseDirection: Boolean = false
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "marioScrollable"
+        properties["orientation"] = orientation
+        properties["state"] = state
+        properties["enabled"] = enabled
+        properties["reverseDirection"] = reverseDirection
+        properties["pivotOffsets"] = pivotOffsets
+    },
+    factory = {
+        val coroutineScope = rememberCoroutineScope()
+        val keepFocusedChildInViewModifier =
+            remember(coroutineScope, orientation, state, reverseDirection) {
+                ContentInViewModifier(
+                    coroutineScope, orientation, state, reverseDirection, pivotOffsets)
+            }
+
+        Modifier
+            .focusGroup()
+            .then(keepFocusedChildInViewModifier.modifier)
+            .pointerScrollable(
+                orientation,
+                reverseDirection,
+                state,
+                enabled
+            )
+            .then(if (enabled) ModifierLocalScrollableContainerProvider else Modifier)
+    }
+)
+
+@Suppress("ComposableModifierFactory")
+@Composable
+private fun Modifier.pointerScrollable(
+    orientation: Orientation,
+    reverseDirection: Boolean,
+    controller: ScrollableState,
+    enabled: Boolean
+): Modifier {
+    val nestedScrollDispatcher = remember { mutableStateOf(NestedScrollDispatcher()) }
+    val scrollLogic = rememberUpdatedState(
+        ScrollingLogic(
+            orientation,
+            reverseDirection,
+            controller
+        )
+    )
+    val nestedScrollConnection = remember(enabled) {
+        scrollableNestedScrollConnection(scrollLogic, enabled)
+    }
+
+    return this.nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
+}
+
+private class ScrollingLogic(
+    val orientation: Orientation,
+    val reverseDirection: Boolean,
+    val scrollableState: ScrollableState,
+) {
+    private fun Float.toOffset(): Offset = when {
+        this == 0f -> Offset.Zero
+        orientation == Horizontal -> Offset(this, 0f)
+        else -> Offset(0f, this)
+    }
+
+    private fun Offset.toFloat(): Float =
+        if (orientation == Horizontal) this.x else this.y
+    private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
+
+    fun performRawScroll(scroll: Offset): Offset {
+        return if (scrollableState.isScrollInProgress) {
+            Offset.Zero
+        } else {
+            scrollableState.dispatchRawDelta(scroll.toFloat().reverseIfNeeded())
+                .reverseIfNeeded().toOffset()
+        }
+    }
+}
+
+private fun scrollableNestedScrollConnection(
+    scrollLogic: State<ScrollingLogic>,
+    enabled: Boolean
+): NestedScrollConnection = object : NestedScrollConnection {
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset = if (enabled) {
+        scrollLogic.value.performRawScroll(available)
+    } else {
+        Offset.Zero
+    }
+}
+
+/**
+ * Handles any logic related to bringing or keeping content in view, including
+ * [BringIntoViewResponder] and ensuring the focused child stays in view when the scrollable area
+ * is shrunk.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+private class ContentInViewModifier(
+    private val scope: CoroutineScope,
+    private val orientation: Orientation,
+    private val scrollableState: ScrollableState,
+    private val reverseDirection: Boolean,
+    private val pivotOffsets: PivotOffsets
+) : BringIntoViewResponder, OnRemeasuredModifier, OnPlacedModifier {
+    private var focusedChild: LayoutCoordinates? = null
+    private var coordinates: LayoutCoordinates? = null
+    private var oldSize: IntSize? = null
+
+    val modifier: Modifier = this
+        .onFocusedBoundsChanged { focusedChild = it }
+        .bringIntoViewResponder(this)
+
+    override fun onRemeasured(size: IntSize) {
+        val coordinates = coordinates
+        val oldSize = oldSize
+        // We only care when this node becomes smaller than it previously was, so don't care about
+        // the initial measurement.
+        if (oldSize != null && oldSize != size && coordinates?.isAttached == true) {
+            onSizeChanged(coordinates, oldSize)
+        }
+        this.oldSize = size
+    }
+
+    override fun onPlaced(coordinates: LayoutCoordinates) {
+        this.coordinates = coordinates
+    }
+
+    override fun calculateRectForParent(localRect: Rect): Rect {
+        val oldSize = checkNotNull(oldSize) {
+            "Expected BringIntoViewRequester to not be used before parents are placed."
+        }
+        // oldSize will only be null before the initial measurement.
+        return computeDestination(localRect, oldSize, pivotOffsets)
+    }
+
+    override suspend fun bringChildIntoView(localRect: Rect) {
+        performBringIntoView(localRect, calculateRectForParent(localRect))
+    }
+
+    private fun onSizeChanged(coordinates: LayoutCoordinates, oldSize: IntSize) {
+        val containerShrunk = if (orientation == Horizontal) {
+            coordinates.size.width < oldSize.width
+        } else {
+            coordinates.size.height < oldSize.height
+        }
+        // If the container is growing, then if the focused child is only partially visible it will
+        // soon be _more_ visible, so don't scroll.
+        if (!containerShrunk) return
+
+        val focusedBounds = focusedChild
+            ?.let { coordinates.localBoundingBoxOf(it, clipBounds = false) }
+            ?: return
+        val myOldBounds = Rect(Offset.Zero, oldSize.toSize())
+        val adjustedBounds = computeDestination(focusedBounds, coordinates.size, pivotOffsets)
+        val wasVisible = myOldBounds.overlaps(focusedBounds)
+        val isFocusedChildClipped = adjustedBounds != focusedBounds
+
+        if (wasVisible && isFocusedChildClipped) {
+            scope.launch {
+                performBringIntoView(focusedBounds, adjustedBounds)
+            }
+        }
+    }
+
+    /**
+     * Compute the destination given the source rectangle and current bounds.
+     *
+     * @param source The bounding box of the item that sent the request to be brought into view.
+     * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+     * from the pivot defined by the parentOffset.
+     * @return the destination rectangle.
+     */
+    private fun computeDestination(
+        source: Rect,
+        intSize: IntSize,
+        pivotOffsets: PivotOffsets
+    ): Rect {
+        val size = intSize.toSize()
+        return when (orientation) {
+            Vertical ->
+                source.translate(
+                    0f,
+                    relocationDistance(source.top, source.bottom, size.height, pivotOffsets))
+            Horizontal ->
+                source.translate(
+                    relocationDistance(source.left, source.right, size.width, pivotOffsets),
+                    0f)
+        }
+    }
+
+    /**
+     * Using the source and destination bounds, perform an animated scroll.
+     */
+    private suspend fun performBringIntoView(source: Rect, destination: Rect) {
+        val offset = when (orientation) {
+            Vertical -> source.top - destination.top
+            Horizontal -> source.left - destination.left
+        }
+        val scrollDelta = if (reverseDirection) -offset else offset
+
+        // Note that this results in weird behavior if called before the previous
+        // performBringIntoView finishes due to b/220119990.
+        scrollableState.animateScrollBy(scrollDelta)
+    }
+
+    /**
+     * Calculate the offset needed to bring one of the edges into view. The leadingEdge is the side
+     * closest to the origin (For the x-axis this is 'left', for the y-axis this is 'top').
+     * The trailing edge is the other side (For the x-axis this is 'right', for the y-axis this is
+     * 'bottom').
+     */
+    private fun relocationDistance(
+        leadingEdgeOfItemRequestingFocus: Float,
+        trailingEdgeOfItemRequestingFocus: Float,
+        parentSize: Float,
+        pivotOffsets: PivotOffsets
+    ): Float {
+        val totalWidthOfItemRequestingFocus =
+            trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus
+        val pivotOfItemRequestingFocus =
+            pivotOffsets.childFraction * totalWidthOfItemRequestingFocus
+        val intendedLocationOfItemRequestingFocus = parentSize * pivotOffsets.parentFraction
+
+        return leadingEdgeOfItemRequestingFocus - intendedLocationOfItemRequestingFocus +
+            pivotOfItemRequestingFocus
+    }
+}
+
+// TODO: b/203141462 - make this public and move it to ui
+/**
+ * Whether this modifier is inside a scrollable container, provided by [Modifier.marioScrollable].
+ * Defaults to false.
+ */
+internal val ModifierLocalScrollableContainer = modifierLocalOf { false }
+
+private object ModifierLocalScrollableContainerProvider : ModifierLocalProvider<Boolean> {
+    override val key = ModifierLocalScrollableContainer
+    override val value = true
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
new file mode 100644
index 0000000..2700311
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation
+
+/**
+ * Holds the offsets needed for mario-scrolling.
+ *
+ * {@property parentFraction} defines the offset of the starting edge of the child
+ * element from the starting edge of the parent element. This value should be between 0 and 1.
+ *
+ * {@property childFraction} defines the offset of the starting edge of the child from
+ * the pivot defined by parentFraction. This value should be between 0 and 1.
+ */
+class PivotOffsets constructor(
+    val parentFraction: Float = 0.3f,
+    val childFraction: Float = 0f
+) {
+    init {
+        validateFraction(parentFraction)
+        validateFraction(childFraction)
+    }
+
+    /* Verify that the fraction passed in lies between 0 and 1 */
+    private fun validateFraction(fraction: Float): Float {
+        if (fraction in 0.0..1.0)
+            return fraction
+        else
+            throw IllegalArgumentException(
+                "OffsetFractions should be between 0 and 1. $fraction is not between 0 and 1.")
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is PivotOffsets) return false
+
+        if (parentFraction != other.parentFraction) return false
+        if (childFraction != other.childFraction) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = parentFraction.hashCode()
+        result = 31 * result + childFraction.hashCode()
+        return result
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt
new file mode 100644
index 0000000..d0611207
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo.Interval
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.tv.foundation.lazy.list.TvLazyListState
+
+/**
+ * This modifier is used to measure and place additional items when the lazyList receives a
+ * request to layout items beyond the visible bounds.
+ */
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.lazyListBeyondBoundsModifier(
+    state: TvLazyListState,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    reverseLayout: Boolean,
+): Modifier {
+    val layoutDirection = LocalLayoutDirection.current
+    return this then remember(state, beyondBoundsInfo, reverseLayout, layoutDirection) {
+        LazyListBeyondBoundsModifierLocal(state, beyondBoundsInfo, reverseLayout, layoutDirection)
+    }
+}
+
+private class LazyListBeyondBoundsModifierLocal(
+    private val state: TvLazyListState,
+    private val beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    private val reverseLayout: Boolean,
+    private val layoutDirection: LayoutDirection
+) : ModifierLocalProvider<BeyondBoundsLayout?>, BeyondBoundsLayout {
+    override val key: ProvidableModifierLocal<BeyondBoundsLayout?>
+        get() = ModifierLocalBeyondBoundsLayout
+    override val value: BeyondBoundsLayout
+        get() = this
+
+    override fun <T> layout(
+        direction: BeyondBoundsLayout.LayoutDirection,
+        block: BeyondBoundsScope.() -> T?
+    ): T? {
+        // We use a new interval each time because this function is re-entrant.
+        var interval = beyondBoundsInfo.addInterval(
+            state.firstVisibleItemIndex,
+            state.layoutInfo.visibleItemsInfo.last().index
+        )
+
+        var found: T? = null
+        while (found == null && interval.hasMoreContent(direction)) {
+
+            // Add one extra beyond bounds item.
+            interval = addNextInterval(interval, direction).also {
+                beyondBoundsInfo.removeInterval(interval)
+            }
+            state.remeasurement?.forceRemeasure()
+
+            // When we invoke this block, the beyond bounds items are present.
+            found = block.invoke(
+                object : BeyondBoundsScope {
+                    override val hasMoreContent: Boolean
+                        get() = interval.hasMoreContent(direction)
+                }
+            )
+        }
+
+        // Dispose the items that are beyond the visible bounds.
+        beyondBoundsInfo.removeInterval(interval)
+        state.remeasurement?.forceRemeasure()
+        return found
+    }
+
+    private fun addNextInterval(
+        currentInterval: Interval,
+        direction: BeyondBoundsLayout.LayoutDirection
+    ): Interval {
+        var start = currentInterval.start
+        var end = currentInterval.end
+        when (direction) {
+            Before -> start--
+            After -> end++
+            Above -> if (reverseLayout) end++ else start--
+            Below -> if (reverseLayout) start-- else end++
+            Left -> when (layoutDirection) {
+                Ltr -> if (reverseLayout) end++ else start--
+                Rtl -> if (reverseLayout) start-- else end++
+            }
+            Right -> when (layoutDirection) {
+                Ltr -> if (reverseLayout) start-- else end++
+                Rtl -> if (reverseLayout) end++ else start--
+            }
+            else -> unsupportedDirection()
+        }
+        return beyondBoundsInfo.addInterval(start, end)
+    }
+
+    private fun Interval.hasMoreContent(direction: BeyondBoundsLayout.LayoutDirection): Boolean {
+        fun hasMoreItemsBefore() = start > 0
+        fun hasMoreItemsAfter() = end < state.layoutInfo.totalItemsCount - 1
+        return when (direction) {
+            Before -> hasMoreItemsBefore()
+            After -> hasMoreItemsAfter()
+            Above -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+            Below -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+            Left -> when (layoutDirection) {
+                Ltr -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+                Rtl -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+            }
+            Right -> when (layoutDirection) {
+                Ltr -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+                Rtl -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+            }
+            else -> unsupportedDirection()
+        }
+    }
+}
+
+private fun unsupportedDirection(): Nothing = error(
+    "Lazy list does not support beyond bounds layout for the specified direction"
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt
new file mode 100644
index 0000000..05005c8
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.compose.runtime.collection.mutableVectorOf
+
+/**
+ * This data structure is used to save information about the number of "beyond bounds items"
+ * that we want to compose. These items are not within the visible bounds of the lazylist,
+ * but we compose them because they are explicitly requested through the
+ * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout].
+ *
+ * When the LazyList receives a
+ * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds] request to
+ * layout items beyond visible bounds, it creates an instance of [LazyListBeyondBoundsInfo] by using
+ * the [addInterval] function. This returns the interval of items that are currently composed,
+ * and we can edit this interval to control the number of beyond bounds items.
+ *
+ * There can be multiple intervals created at the same time, and LazyList merges all the
+ * intervals to calculate the effective beyond bounds items.
+ *
+ * The [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout] is designed to be
+ * synchronous, so once you are done using the items, call [removeInterval] to remove
+ * the extra items you had requested.
+ *
+ * Note that when you clear an interval, the items in that interval might not be cleared right
+ * away if another interval was created that has the same items. This is done to support two use
+ * cases:
+ *
+ * 1. To allow items to be pinned while they are being scrolled into view.
+ *
+ * 2. To allow users to call
+ * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds]
+ * from within the completion block of another searchBeyondBounds call.
+ */
+internal class LazyListBeyondBoundsInfo {
+    private val beyondBoundsItems = mutableVectorOf<Interval>()
+
+    /**
+     * Create a beyond bounds interval. This can be used to specify which composed items we want to
+     * retain. For instance, it can be used to force the measuring of items that are beyond the
+     * visible bounds of a lazy list.
+     *
+     * @param start The starting index (inclusive) for this interval.
+     * @param end The ending index (inclusive) for this interval.
+     *
+     * @return An interval that specifies which items we want to retain.
+     */
+    fun addInterval(start: Int, end: Int): Interval {
+        return Interval(start, end).apply {
+            beyondBoundsItems.add(this)
+        }
+    }
+
+    /**
+     * Clears the specified interval. Use this to remove the interval created by [addInterval].
+     */
+    fun removeInterval(interval: Interval) {
+        beyondBoundsItems.remove(interval)
+    }
+
+    /**
+     * Returns true if there are beyond bounds intervals.
+     */
+    fun hasIntervals(): Boolean = beyondBoundsItems.isNotEmpty()
+
+    /**
+     *  The effective start index after merging all the current intervals.
+     */
+    val start: Int
+        get() {
+            var minIndex = beyondBoundsItems.first().start
+            beyondBoundsItems.forEach {
+                if (it.start < minIndex) {
+                    minIndex = it.start
+                }
+            }
+            require(minIndex >= 0)
+            return minIndex
+        }
+
+    /**
+     *  The effective end index after merging all the current intervals.
+     */
+    val end: Int
+        get() {
+            var maxIndex = beyondBoundsItems.first().end
+            beyondBoundsItems.forEach {
+                if (it.end > maxIndex) {
+                    maxIndex = it.end
+                }
+            }
+            return maxIndex
+        }
+
+    /**
+     * The Interval used to implement [LazyListBeyondBoundsInfo].
+     */
+    internal data class Interval(
+        /** The start index for the interval. */
+        val start: Int,
+
+        /** The end index for the interval. */
+        val end: Int
+    ) {
+        init {
+            require(start >= 0)
+            require(end >= start)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt
new file mode 100644
index 0000000..a726ffb
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.ModifierLocalPinnableParent
+import androidx.compose.foundation.lazy.layout.PinnableParent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.tv.foundation.lazy.list.TvLazyListState
+
+/**
+ * This is a temporary placeholder implementation of pinning until we implement b/195049010.
+ */
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.lazyListPinningModifier(
+    state: TvLazyListState,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo
+): Modifier {
+    return this then remember(state, beyondBoundsInfo) {
+        LazyListPinningModifier(state, beyondBoundsInfo)
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class LazyListPinningModifier(
+    private val state: TvLazyListState,
+    private val beyondBoundsInfo: LazyListBeyondBoundsInfo,
+) : ModifierLocalProvider<PinnableParent?>, ModifierLocalConsumer, PinnableParent {
+    var pinnableGrandParent: PinnableParent? = null
+
+    override val key: ProvidableModifierLocal<PinnableParent?>
+        get() = ModifierLocalPinnableParent
+
+    override val value: PinnableParent
+        get() = this
+
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+        pinnableGrandParent = with(scope) { ModifierLocalPinnableParent.current }
+    }
+
+    override fun pinItems(): PinnableParent.PinnedItemsHandle = with(beyondBoundsInfo) {
+        if (hasIntervals()) {
+            object : PinnableParent.PinnedItemsHandle {
+                val parentPinnedItemsHandle = pinnableGrandParent?.pinItems()
+                val interval = addInterval(start, end)
+                override fun unpin() {
+                    removeInterval(interval)
+                    parentPinnedItemsHandle?.unpin()
+                    state.remeasurement?.forceRemeasure()
+                }
+            }
+        } else {
+            pinnableGrandParent?.pinItems() ?: EmptyPinnedItemsHandle
+        }
+    }
+
+    companion object {
+        private val EmptyPinnedItemsHandle = object : PinnableParent.PinnedItemsHandle {
+            override fun unpin() {}
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt
new file mode 100644
index 0000000..8ae2a25
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+/**
+ * Represents a line index in the lazy grid.
+ */
+@Suppress("NOTHING_TO_INLINE")
+@kotlin.jvm.JvmInline
+internal value class LineIndex(val value: Int) {
+    inline operator fun inc(): LineIndex = LineIndex(value + 1)
+    inline operator fun dec(): LineIndex = LineIndex(value - 1)
+    inline operator fun plus(i: Int): LineIndex = LineIndex(value + i)
+    inline operator fun minus(i: Int): LineIndex = LineIndex(value - i)
+    inline operator fun minus(i: LineIndex): LineIndex = LineIndex(value - i.value)
+    inline operator fun compareTo(other: LineIndex): Int = value - other.value
+}
+
+/**
+ * Represents an item index in the lazy grid.
+ */
+@Suppress("NOTHING_TO_INLINE")
+@kotlin.jvm.JvmInline
+internal value class ItemIndex(val value: Int) {
+    inline operator fun inc(): ItemIndex = ItemIndex(value + 1)
+    inline operator fun dec(): ItemIndex = ItemIndex(value - 1)
+    inline operator fun plus(i: Int): ItemIndex = ItemIndex(value + i)
+    inline operator fun minus(i: Int): ItemIndex = ItemIndex(value - i)
+    inline operator fun minus(i: ItemIndex): ItemIndex = ItemIndex(value - i.value)
+    inline operator fun compareTo(other: ItemIndex): Int = value - other.value
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
new file mode 100644
index 0000000..e84fadf
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+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.Modifier
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.marioScrollable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun LazyGrid(
+    /** Modifier to be applied for the inner layout */
+    modifier: Modifier = Modifier,
+    /** State controlling the scroll position */
+    state: TvLazyGridState,
+    /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */
+    slotSizesSums: Density.(Constraints) -> List<Int>,
+    /** The inner padding to be added for the whole content (not for each individual item) */
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean = false,
+    /** The layout orientation of the grid */
+    isVertical: Boolean,
+    /** Whether scrolling via the user gestures is allowed. */
+    userScrollEnabled: Boolean,
+    /** The vertical arrangement for items/lines. */
+    verticalArrangement: Arrangement.Vertical,
+    /** The horizontal arrangement for items/lines. */
+    horizontalArrangement: Arrangement.Horizontal,
+    /** offsets of child element within the parent and starting edge of the child from the pivot
+     * defined by the parentOffset */
+    pivotOffsets: PivotOffsets,
+    /** The content of the grid */
+    content: TvLazyGridScope.() -> Unit
+) {
+    val itemProvider = rememberItemProvider(state, content)
+
+    val scope = rememberCoroutineScope()
+    val placementAnimator = remember(state, isVertical) {
+        LazyGridItemPlacementAnimator(scope, isVertical)
+    }
+    state.placementAnimator = placementAnimator
+
+    val measurePolicy = rememberLazyGridMeasurePolicy(
+        itemProvider,
+        state,
+        slotSizesSums,
+        contentPadding,
+        reverseLayout,
+        isVertical,
+        horizontalArrangement,
+        verticalArrangement,
+        placementAnimator
+    )
+
+    state.isVertical = isVertical
+
+    ScrollPositionUpdater(itemProvider, state)
+
+    val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
+    LazyLayout(
+        modifier = modifier
+            .then(state.remeasurementModifier)
+            .then(state.awaitLayoutModifier)
+            .lazyGridSemantics(
+                itemProvider = itemProvider,
+                state = state,
+                coroutineScope = scope,
+                isVertical = isVertical,
+                reverseScrolling = reverseLayout,
+                userScrollEnabled = userScrollEnabled
+            )
+            .clipScrollableContainer(orientation)
+            .marioScrollable(
+                orientation = orientation,
+                reverseDirection = run {
+                    // A finger moves with the content, not with the viewport. Therefore,
+                    // always reverse once to have "natural" gesture that goes reversed to layout
+                    var reverseDirection = !reverseLayout
+                    // But if rtl and horizontal, things move the other way around
+                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+                    if (isRtl && !isVertical) {
+                        reverseDirection = !reverseDirection
+                    }
+                    reverseDirection
+                },
+                state = state,
+                enabled = userScrollEnabled,
+                pivotOffsets = pivotOffsets
+            ),
+        prefetchState = state.prefetchState,
+        measurePolicy = measurePolicy,
+        itemProvider = itemProvider
+    )
+}
+
+/** Extracted to minimize the recomposition scope */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ScrollPositionUpdater(
+    itemProvider: LazyGridItemProvider,
+    state: TvLazyGridState
+) {
+    if (itemProvider.itemCount > 0) {
+        state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberLazyGridMeasurePolicy(
+    /** Items provider of the list. */
+    itemProvider: LazyGridItemProvider,
+    /** The state of the list. */
+    state: TvLazyGridState,
+    /** Prefix sums of cross axis sizes of slots of the grid. */
+    slotSizesSums: Density.(Constraints) -> List<Int>,
+    /** The inner padding to be added for the whole content(nor for each individual item) */
+    contentPadding: PaddingValues,
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean,
+    /** The layout orientation of the list */
+    isVertical: Boolean,
+    /** The horizontal arrangement for items. Required when isVertical is false */
+    horizontalArrangement: Arrangement.Horizontal? = null,
+    /** The vertical arrangement for items. Required when isVertical is true */
+    verticalArrangement: Arrangement.Vertical? = null,
+    /** Item placement animator. Should be notified with the measuring result */
+    placementAnimator: LazyGridItemPlacementAnimator
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+    state,
+    slotSizesSums,
+    contentPadding,
+    reverseLayout,
+    isVertical,
+    horizontalArrangement,
+    verticalArrangement,
+    placementAnimator
+) {
+    { containerConstraints ->
+        checkScrollableContainerConstraints(
+            containerConstraints,
+            if (isVertical) Orientation.Vertical else Orientation.Horizontal
+        )
+
+        // resolve content paddings
+        val startPadding =
+            if (isVertical) {
+                contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+            }
+
+        val endPadding =
+            if (isVertical) {
+                contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+            }
+        val topPadding = contentPadding.calculateTopPadding().roundToPx()
+        val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+        val totalVerticalPadding = topPadding + bottomPadding
+        val totalHorizontalPadding = startPadding + endPadding
+        val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+        val beforeContentPadding = when {
+            isVertical && !reverseLayout -> topPadding
+            isVertical && reverseLayout -> bottomPadding
+            !isVertical && !reverseLayout -> startPadding
+            else -> endPadding // !isVertical && reverseLayout
+        }
+        val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+        val contentConstraints =
+            containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+        state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+
+        val spanLayoutProvider = itemProvider.spanLayoutProvider
+        val resolvedSlotSizesSums = slotSizesSums(containerConstraints)
+        spanLayoutProvider.slotsPerLine = resolvedSlotSizesSums.size
+
+        // Update the state's cached Density and slotsPerLine
+        state.density = this
+        state.slotsPerLine = resolvedSlotSizesSums.size
+
+        val spaceBetweenLinesDp = if (isVertical) {
+            requireNotNull(verticalArrangement).spacing
+        } else {
+            requireNotNull(horizontalArrangement).spacing
+        }
+        val spaceBetweenLines = spaceBetweenLinesDp.roundToPx()
+        val spaceBetweenSlotsDp = if (isVertical) {
+            horizontalArrangement?.spacing ?: 0.dp
+        } else {
+            verticalArrangement?.spacing ?: 0.dp
+        }
+        val spaceBetweenSlots = spaceBetweenSlotsDp.roundToPx()
+
+        val itemsCount = itemProvider.itemCount
+
+        // can be negative if the content padding is larger than the max size from constraints
+        val mainAxisAvailableSize = if (isVertical) {
+            containerConstraints.maxHeight - totalVerticalPadding
+        } else {
+            containerConstraints.maxWidth - totalHorizontalPadding
+        }
+        val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+            IntOffset(startPadding, topPadding)
+        } else {
+            // When layout is reversed and paddings together take >100% of the available space,
+            // layout size is coerced to 0 when positioning. To take that space into account,
+            // we offset start padding by negative space between paddings.
+            IntOffset(
+                if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+                if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+            )
+        }
+
+        val measuredItemProvider = LazyMeasuredItemProvider(
+            itemProvider,
+            this,
+            spaceBetweenLines
+        ) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
+            LazyMeasuredItem(
+                index = index,
+                key = key,
+                isVertical = isVertical,
+                crossAxisSize = crossAxisSize,
+                mainAxisSpacing = mainAxisSpacing,
+                reverseLayout = reverseLayout,
+                layoutDirection = layoutDirection,
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                visualOffset = visualItemOffset,
+                placeables = placeables,
+                placementAnimator = placementAnimator
+            )
+        }
+        val measuredLineProvider = LazyMeasuredLineProvider(
+            isVertical,
+            resolvedSlotSizesSums,
+            spaceBetweenSlots,
+            itemsCount,
+            spaceBetweenLines,
+            measuredItemProvider,
+            spanLayoutProvider
+        ) { index, items, spans, mainAxisSpacing ->
+            LazyMeasuredLine(
+                index = index,
+                items = items,
+                spans = spans,
+                isVertical = isVertical,
+                slotsPerLine = resolvedSlotSizesSums.size,
+                layoutDirection = layoutDirection,
+                mainAxisSpacing = mainAxisSpacing,
+                crossAxisSpacing = spaceBetweenSlots
+            )
+        }
+        state.prefetchInfoRetriever = { line ->
+            val lineConfiguration = spanLayoutProvider.getLineConfiguration(line.value)
+            var index = ItemIndex(lineConfiguration.firstItemIndex)
+            var slot = 0
+            val result = ArrayList<Pair<Int, Constraints>>(lineConfiguration.spans.size)
+            lineConfiguration.spans.fastForEach {
+                val span = it.currentLineSpan
+                result.add(index.value to measuredLineProvider.childConstraints(slot, span))
+                ++index
+                slot += span
+            }
+            result
+        }
+
+        val firstVisibleLineIndex: LineIndex
+        val firstVisibleLineScrollOffset: Int
+        Snapshot.withoutReadObservation {
+            if (state.firstVisibleItemIndex < itemsCount || itemsCount <= 0) {
+                firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(
+                    state.firstVisibleItemIndex
+                )
+                firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset
+            } else {
+                // the data set has been updated and now we have less items that we were
+                // scrolled to before
+                firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(itemsCount - 1)
+                firstVisibleLineScrollOffset = 0
+            }
+        }
+        measureLazyGrid(
+            itemsCount = itemsCount,
+            measuredLineProvider = measuredLineProvider,
+            measuredItemProvider = measuredItemProvider,
+            mainAxisAvailableSize = mainAxisAvailableSize,
+            slotsPerLine = resolvedSlotSizesSums.size,
+            beforeContentPadding = beforeContentPadding,
+            afterContentPadding = afterContentPadding,
+            firstVisibleLineIndex = firstVisibleLineIndex,
+            firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
+            scrollToBeConsumed = state.scrollToBeConsumed,
+            constraints = contentConstraints,
+            isVertical = isVertical,
+            verticalArrangement = verticalArrangement,
+            horizontalArrangement = horizontalArrangement,
+            reverseLayout = reverseLayout,
+            density = this,
+            placementAnimator = placementAnimator,
+            layout = { width, height, placement ->
+                layout(
+                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                    containerConstraints.constrainHeight(height + totalVerticalPadding),
+                    emptyMap(),
+                    placement
+                )
+            }
+        ).also { state.applyMeasureResult(it) }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
new file mode 100644
index 0000000..9321d6d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+
+/**
+ * A lazy vertical grid layout. It composes only visible rows of the grid.
+ *
+ * @param columns describes the count and the size of the grid's columns,
+ * see [TvGridCells] doc for more information
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items will be
+ * laid out in the reverse order  and [TvLazyGridState.firstVisibleItemIndex] == 0 means
+ * that grid is scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
+ * [verticalArrangement],
+ * e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321### (bottom).
+ * @param verticalArrangement The vertical arrangement of the layout's children
+ * @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param pivotOffsets offsets that are used when implementing Mario Scrolling
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content the [TvLazyGridScope] which describes the content
+ */
+@Composable
+fun TvLazyVerticalGrid(
+    columns: TvGridCells,
+    modifier: Modifier = Modifier,
+    state: TvLazyGridState = rememberTvLazyGridState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    content: TvLazyGridScope.() -> Unit
+) {
+    val slotSizesSums = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding)
+    LazyGrid(
+        slotSizesSums = slotSizesSums,
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        isVertical = true,
+        horizontalArrangement = horizontalArrangement,
+        verticalArrangement = verticalArrangement,
+        userScrollEnabled = userScrollEnabled,
+        content = content,
+        pivotOffsets = pivotOffsets
+    )
+}
+
+/**
+ * A lazy horizontal grid layout. It composes only visible columns of the grid.
+ *
+ * @param rows a class describing how cells form rows, see [TvGridCells] doc for more information
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [TvLazyGridState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param verticalArrangement The vertical arrangement of the layout's children
+ * @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param pivotOffsets offsets that are used when implementing Mario Scrolling
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content the [TvLazyGridScope] which describes the content
+ */
+@Composable
+fun TvLazyHorizontalGrid(
+    rows: TvGridCells,
+    modifier: Modifier = Modifier,
+    state: TvLazyGridState = rememberTvLazyGridState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
+    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    content: TvLazyGridScope.() -> Unit
+) {
+    val slotSizesSums = rememberRowHeightSums(rows, verticalArrangement, contentPadding)
+    LazyGrid(
+        slotSizesSums = slotSizesSums,
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        isVertical = false,
+        horizontalArrangement = horizontalArrangement,
+        verticalArrangement = verticalArrangement,
+        userScrollEnabled = userScrollEnabled,
+        pivotOffsets = pivotOffsets,
+        content = content
+    )
+}
+
+/** Returns prefix sums of column widths. */
+@Composable
+private fun rememberColumnWidthSums(
+    columns: TvGridCells,
+    horizontalArrangement: Arrangement.Horizontal,
+    contentPadding: PaddingValues
+) = remember<Density.(Constraints) -> List<Int>>(
+    columns,
+    horizontalArrangement,
+    contentPadding,
+) {
+    { constraints ->
+        require(constraints.maxWidth != Constraints.Infinity) {
+            "LazyVerticalGrid's width should be bound by parent."
+        }
+        val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
+            contentPadding.calculateEndPadding(LayoutDirection.Ltr)
+        val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
+        with(columns) {
+            calculateCrossAxisCellSizes(
+                gridWidth,
+                horizontalArrangement.spacing.roundToPx()
+            ).toMutableList().apply {
+                for (i in 1 until size) {
+                    this[i] += this[i - 1]
+                }
+            }
+        }
+    }
+}
+
+/** Returns prefix sums of row heights. */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberRowHeightSums(
+    rows: TvGridCells,
+    verticalArrangement: Arrangement.Vertical,
+    contentPadding: PaddingValues
+) = remember<Density.(Constraints) -> List<Int>>(
+    rows,
+    verticalArrangement,
+    contentPadding,
+) {
+    { constraints ->
+        require(constraints.maxHeight != Constraints.Infinity) {
+            "LazyHorizontalGrid's height should be bound by parent."
+        }
+        val verticalPadding = contentPadding.calculateTopPadding() +
+            contentPadding.calculateBottomPadding()
+        val gridHeight = constraints.maxHeight - verticalPadding.roundToPx()
+        with(rows) {
+            calculateCrossAxisCellSizes(
+                gridHeight,
+                verticalArrangement.spacing.roundToPx()
+            ).toMutableList().apply {
+                for (i in 1 until size) {
+                    this[i] += this[i - 1]
+                }
+            }
+        }
+    }
+}
+
+/**
+ * This class describes the count and the sizes of columns in vertical grids,
+ * or rows in horizontal grids.
+ */
+@Stable
+interface TvGridCells {
+    /**
+     * Calculates the number of cells and their cross axis size based on
+     * [availableSize] and [spacing].
+     *
+     * For example, in vertical grids, [spacing] is passed from the grid's [Arrangement.Horizontal].
+     * The [Arrangement.Horizontal] will also be used to arrange items in a row if the grid is wider
+     * than the calculated sum of columns.
+     *
+     * Note that the calculated cross axis sizes will be considered in an RTL-aware manner --
+     * if the grid is vertical and the layout direction is RTL, the first width in the returned
+     * list will correspond to the rightmost column.
+     *
+     * @param availableSize available size on cross axis, e.g. width of [TvLazyVerticalGrid].
+     * @param spacing cross axis spacing, e.g. horizontal spacing for [TvLazyVerticalGrid].
+     * The spacing is passed from the corresponding [Arrangement] param of the lazy grid.
+     */
+    fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List<Int>
+
+    /**
+     * Defines a grid with fixed number of rows or columns.
+     *
+     * For example, for the vertical [TvLazyVerticalGrid] Fixed(3) would mean that
+     * there are 3 columns 1/3 of the parent width.
+     */
+    class Fixed(private val count: Int) : TvGridCells {
+        init {
+            require(count > 0)
+        }
+
+        override fun Density.calculateCrossAxisCellSizes(
+            availableSize: Int,
+            spacing: Int
+        ): List<Int> {
+            return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
+        }
+
+        override fun hashCode(): Int {
+            return -count // Different sign from Adaptive.
+        }
+
+        override fun equals(other: Any?): Boolean {
+            return other is Fixed && count == other.count
+        }
+    }
+
+    /**
+     * Defines a grid with as many rows or columns as possible on the condition that
+     * every cell has at least [minSize] space and all extra space distributed evenly.
+     *
+     * For example, for the vertical [TvLazyVerticalGrid] Adaptive(20.dp) would mean that
+     * there will be as many columns as possible and every column will be at least 20.dp
+     * and all the columns will have equal width. If the screen is 88.dp wide then
+     * there will be 4 columns 22.dp each.
+     */
+    class Adaptive(private val minSize: Dp) : TvGridCells {
+        init {
+            require(minSize > 0.dp)
+        }
+
+        override fun Density.calculateCrossAxisCellSizes(
+            availableSize: Int,
+            spacing: Int
+        ): List<Int> {
+            val count = maxOf((availableSize + spacing) / (minSize.roundToPx() + spacing), 1)
+            return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
+        }
+
+        override fun hashCode(): Int {
+            return minSize.hashCode()
+        }
+
+        override fun equals(other: Any?): Boolean {
+            return other is Adaptive && minSize == other.minSize
+        }
+    }
+}
+
+private fun calculateCellsCrossAxisSizeImpl(
+    gridSize: Int,
+    slotCount: Int,
+    spacing: Int
+): List<Int> {
+    val gridSizeWithoutSpacing = gridSize - spacing * (slotCount - 1)
+    val slotSize = gridSizeWithoutSpacing / slotCount
+    val remainingPixels = gridSizeWithoutSpacing % slotCount
+    return List(slotCount) {
+        slotSize + if (it < remainingPixels) 1 else 0
+    }
+}
+
+/**
+ * Receiver scope which is used by [TvLazyVerticalGrid].
+ */
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridScope {
+    /**
+     * Adds a single item to the scope.
+     *
+     * @param key a stable and unique key representing the item. Using the same key
+     * for multiple items in the grid is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the grid will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param span the span of the item. Default is 1x1. It is good practice to leave it `null`
+     * when this matches the intended behavior, as providing a custom implementation impacts
+     * performance
+     * @param contentType the type of the content of this item. The item compositions of the same
+     * type could be reused more efficiently. Note that null is a valid type and items of such
+     * type will be considered compatible.
+     * @param content the content of the item
+     */
+    fun item(
+        key: Any? = null,
+        span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)? = null,
+        contentType: Any? = null,
+        content: @Composable TvLazyGridItemScope.() -> Unit
+    )
+
+    /**
+     * Adds a [count] of items.
+     *
+     * @param count the items count
+     * @param key a factory of stable and unique keys representing the item. Using the same key
+     * for multiple items in the grid is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the grid will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param span define custom spans for the items. Default is 1x1. It is good practice to
+     * leave it `null` when this matches the intended behavior, as providing a custom
+     * implementation impacts performance
+     * @param contentType a factory of the content types for the item. The item compositions of
+     * the same type could be reused more efficiently. Note that null is a valid type and items
+     * of such type will be considered compatible.
+     * @param itemContent the content displayed by a single item
+     */
+    fun items(
+        count: Int,
+        key: ((index: Int) -> Any)? = null,
+        span: (TvLazyGridItemSpanScope.(index: Int) -> TvGridItemSpan)? = null,
+        contentType: (index: Int) -> Any? = { null },
+        itemContent: @Composable TvLazyGridItemScope.(index: Int) -> Unit
+    )
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to
+ * leave it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.items(
+    items: List<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
+    noinline contentType: (item: T) -> Any? = { null },
+    crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(items[index]) } else null,
+    span = if (span != null) { { span(items[it]) } } else null,
+    contentType = { index: Int -> contentType(items[index]) }
+) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.itemsIndexed(
+    items: List<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+    span = if (span != null) { { span(it, items[it]) } } else null,
+    contentType = { index -> contentType(index, items[index]) }
+) {
+    itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.items(
+    items: Array<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
+    noinline contentType: (item: T) -> Any? = { null },
+    crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(items[index]) } else null,
+    span = if (span != null) { { span(items[it]) } } else null,
+    contentType = { index: Int -> contentType(items[index]) }
+) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds an array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.itemsIndexed(
+    items: Array<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+    span = if (span != null) { { span(it, items[it]) } } else null,
+    contentType = { index -> contentType(index, items[index]) }
+) {
+    itemContent(it, items[it])
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
new file mode 100644
index 0000000..223640a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import kotlin.math.absoluteValue
+import kotlin.math.max
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Handles the item placement animations when it is set via
+ * [TvLazyGridItemScope.animateItemPlacement].
+ *
+ * This class is responsible for detecting when item position changed, figuring our start/end
+ * offsets and starting the animations.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridItemPlacementAnimator(
+    private val scope: CoroutineScope,
+    private val isVertical: Boolean
+) {
+    private var slotsPerLine = 0
+
+    // state containing an animation and all relevant info for each item.
+    private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+
+    // snapshot of the key to index map used for the last measuring.
+    private var keyToIndexMap: Map<Any, Int> = emptyMap()
+
+    // keeps the first and the last items positioned in the viewport and their visible part sizes.
+    private var viewportStartItemIndex = -1
+    private var viewportStartItemNotVisiblePartSize = 0
+    private var viewportEndItemIndex = -1
+    private var viewportEndItemNotVisiblePartSize = 0
+
+    // stored to not allocate it every pass.
+    private val positionedKeys = mutableSetOf<Any>()
+
+    /**
+     * Should be called after the measuring so we can detect position changes and start animations.
+     *
+     * Note that this method can compose new item and add it into the [positionedItems] list.
+     */
+    fun onMeasured(
+        consumedScroll: Int,
+        layoutWidth: Int,
+        layoutHeight: Int,
+        slotsPerLine: Int,
+        reverseLayout: Boolean,
+        positionedItems: MutableList<TvLazyGridPositionedItem>,
+        measuredItemProvider: LazyMeasuredItemProvider,
+    ) {
+        if (!positionedItems.fastAny { it.hasAnimations }) {
+            // no animations specified - no work needed
+            reset()
+            return
+        }
+
+        this.slotsPerLine = slotsPerLine
+
+        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+
+        // the consumed scroll is considered as a delta we don't need to animate
+        val notAnimatableDelta = (if (reverseLayout) -consumedScroll else consumedScroll).toOffset()
+
+        val newFirstItem = positionedItems.first()
+        val newLastItem = positionedItems.last()
+
+        positionedItems.fastForEach { item ->
+            val itemInfo = keyToItemInfoMap[item.key] ?: return@fastForEach
+            itemInfo.index = item.index
+            itemInfo.crossAxisSize = item.getCrossAxisSize()
+            itemInfo.crossAxisOffset = item.getCrossAxisOffset()
+        }
+
+        val averageLineMainAxisSize = run {
+            val lineOf: (Int) -> Int = {
+                if (isVertical) positionedItems[it].row else positionedItems[it].column
+            }
+
+            var totalLinesMainAxisSize = 0
+            var linesCount = 0
+
+            var lineStartIndex = 0
+            while (lineStartIndex < positionedItems.size) {
+                val currentLine = lineOf(lineStartIndex)
+                if (currentLine == -1) {
+                    // Filter out exiting items.
+                    ++lineStartIndex
+                    continue
+                }
+
+                var lineMainAxisSize = 0
+                var lineEndIndex = lineStartIndex
+                while (lineEndIndex < positionedItems.size && lineOf(lineEndIndex) == currentLine) {
+                    lineMainAxisSize = max(
+                        lineMainAxisSize,
+                        positionedItems[lineEndIndex].mainAxisSizeWithSpacings
+                    )
+                    ++lineEndIndex
+                }
+
+                totalLinesMainAxisSize += lineMainAxisSize
+                ++linesCount
+
+                lineStartIndex = lineEndIndex
+            }
+
+            totalLinesMainAxisSize / linesCount
+        }
+
+        positionedKeys.clear()
+        // iterate through the items which are visible (without animated offsets)
+        positionedItems.fastForEach { item ->
+            positionedKeys.add(item.key)
+            val itemInfo = keyToItemInfoMap[item.key]
+            if (itemInfo == null) {
+                // there is no state associated with this item yet
+                if (item.hasAnimations) {
+                    val newItemInfo = ItemInfo(
+                        item.index,
+                        item.getCrossAxisSize(),
+                        item.getCrossAxisOffset()
+                    )
+                    val previousIndex = keyToIndexMap[item.key]
+                    val offset = item.placeableOffset
+
+                    val targetPlaceableOffsetMainAxis = if (previousIndex == null) {
+                        // it is a completely new item. no animation is needed
+                        offset.mainAxis
+                    } else {
+                        val fallback = if (!reverseLayout) {
+                            offset.mainAxis
+                        } else {
+                            offset.mainAxis - item.mainAxisSizeWithSpacings
+                        }
+                        calculateExpectedOffset(
+                            index = previousIndex,
+                            mainAxisSizeWithSpacings = item.mainAxisSizeWithSpacings,
+                            averageLineMainAxisSize = averageLineMainAxisSize,
+                            scrolledBy = notAnimatableDelta,
+                            fallback = fallback,
+                            reverseLayout = reverseLayout,
+                            mainAxisLayoutSize = mainAxisLayoutSize
+                        )
+                    }
+                    val targetPlaceableOffset = if (isVertical) {
+                        offset.copy(y = targetPlaceableOffsetMainAxis)
+                    } else {
+                        offset.copy(x = targetPlaceableOffsetMainAxis)
+                    }
+
+                    // populate placeable info list
+                    repeat(item.placeablesCount) { placeableIndex ->
+                        newItemInfo.placeables.add(
+                            PlaceableInfo(
+                                targetPlaceableOffset,
+                                item.getMainAxisSize(placeableIndex)
+                            )
+                        )
+                    }
+                    keyToItemInfoMap[item.key] = newItemInfo
+                    startAnimationsIfNeeded(item, newItemInfo)
+                }
+            } else {
+                if (item.hasAnimations) {
+                    // apply new not animatable offset
+                    itemInfo.notAnimatableDelta += notAnimatableDelta
+                    startAnimationsIfNeeded(item, itemInfo)
+                } else {
+                    // no animation, clean up if needed
+                    keyToItemInfoMap.remove(item.key)
+                }
+            }
+        }
+
+        // previously we were animating items which are visible in the end state so we had to
+        // compare the current state with the state used for the previous measuring.
+        // now we will animate disappearing items so the current state is their starting state
+        // so we can update current viewport start/end items
+
+        if (!reverseLayout) {
+            viewportStartItemIndex = newFirstItem.index
+            viewportStartItemNotVisiblePartSize = newFirstItem.offset.mainAxis
+            viewportEndItemIndex = newLastItem.index
+            viewportEndItemNotVisiblePartSize = newLastItem.offset.mainAxis +
+                newLastItem.lineMainAxisSizeWithSpacings - mainAxisLayoutSize
+        } else {
+            viewportStartItemIndex = newLastItem.index
+            viewportStartItemNotVisiblePartSize = mainAxisLayoutSize -
+                newLastItem.offset.mainAxis - newLastItem.lineMainAxisSize
+            viewportEndItemIndex = newFirstItem.index
+            viewportEndItemNotVisiblePartSize = -newFirstItem.offset.mainAxis +
+                (newFirstItem.lineMainAxisSizeWithSpacings -
+                    if (isVertical) newFirstItem.size.height else newFirstItem.size.width)
+        }
+
+        val iterator = keyToItemInfoMap.iterator()
+        while (iterator.hasNext()) {
+            val entry = iterator.next()
+            if (!positionedKeys.contains(entry.key)) {
+                // found an item which was in our map previously but is not a part of the
+                // positionedItems now
+                val itemInfo = entry.value
+                // apply new not animatable delta for this item
+                itemInfo.notAnimatableDelta += notAnimatableDelta
+
+                val index = measuredItemProvider.keyToIndexMap[entry.key]
+
+                // whether at least one placeable is within the viewport bounds.
+                // this usually means that we will start animation for it right now
+                val withinBounds = itemInfo.placeables.fastAny {
+                    val currentTarget = it.targetOffset + itemInfo.notAnimatableDelta
+                    currentTarget.mainAxis + it.mainAxisSize > 0 &&
+                        currentTarget.mainAxis < mainAxisLayoutSize
+                }
+
+                // whether the animation associated with the item has been finished
+                val isFinished = !itemInfo.placeables.fastAny { it.inProgress }
+
+                if ((!withinBounds && isFinished) ||
+                    index == null ||
+                    itemInfo.placeables.isEmpty()
+                ) {
+                    iterator.remove()
+                } else {
+                    // not sure if this item will end up on the last line or not. assume not,
+                    // therefore leave the mainAxisSpacing to be the default one
+                    val measuredItem = measuredItemProvider.getAndMeasure(
+                        index = ItemIndex(index),
+                        constraints = if (isVertical) {
+                            Constraints.fixedWidth(itemInfo.crossAxisSize)
+                        } else {
+                            Constraints.fixedHeight(itemInfo.crossAxisSize)
+                        }
+                    )
+
+                    // calculate the target offset for the animation.
+                    val absoluteTargetOffset = calculateExpectedOffset(
+                        index = index,
+                        mainAxisSizeWithSpacings = measuredItem.mainAxisSizeWithSpacings,
+                        averageLineMainAxisSize = averageLineMainAxisSize,
+                        scrolledBy = notAnimatableDelta,
+                        fallback = mainAxisLayoutSize,
+                        reverseLayout = reverseLayout,
+                        mainAxisLayoutSize = mainAxisLayoutSize
+                    )
+                    val targetOffset = if (reverseLayout) {
+                        mainAxisLayoutSize - absoluteTargetOffset - measuredItem.mainAxisSize
+                    } else {
+                        absoluteTargetOffset
+                    }
+
+                    val item = measuredItem.position(
+                        targetOffset,
+                        itemInfo.crossAxisOffset,
+                        layoutWidth,
+                        layoutHeight,
+                        TvLazyGridItemInfo.UnknownRow,
+                        TvLazyGridItemInfo.UnknownColumn,
+                        measuredItem.mainAxisSize
+                    )
+                    positionedItems.add(item)
+                    startAnimationsIfNeeded(item, itemInfo)
+                }
+            }
+        }
+
+        keyToIndexMap = measuredItemProvider.keyToIndexMap
+    }
+
+    /**
+     * Returns the current animated item placement offset. By calling it only during the layout
+     * phase we can skip doing remeasure on every animation frame.
+     */
+    fun getAnimatedOffset(
+        key: Any,
+        placeableIndex: Int,
+        minOffset: Int,
+        maxOffset: Int,
+        rawOffset: IntOffset
+    ): IntOffset {
+        val itemInfo = keyToItemInfoMap[key] ?: return rawOffset
+        val item = itemInfo.placeables[placeableIndex]
+        val currentValue = item.animatedOffset.value + itemInfo.notAnimatableDelta
+        val currentTarget = item.targetOffset + itemInfo.notAnimatableDelta
+
+        // cancel the animation if it is fully out of the bounds.
+        if (item.inProgress &&
+            ((currentTarget.mainAxis < minOffset && currentValue.mainAxis < minOffset) ||
+            (currentTarget.mainAxis > maxOffset && currentValue.mainAxis > maxOffset))
+        ) {
+            scope.launch {
+                item.animatedOffset.snapTo(item.targetOffset)
+                item.inProgress = false
+            }
+        }
+
+        return currentValue
+    }
+
+    /**
+     * Should be called when the animations are not needed for the next positions change,
+     * for example when we snap to a new position.
+     */
+    fun reset() {
+        keyToItemInfoMap.clear()
+        keyToIndexMap = emptyMap()
+        viewportStartItemIndex = -1
+        viewportStartItemNotVisiblePartSize = 0
+        viewportEndItemIndex = -1
+        viewportEndItemNotVisiblePartSize = 0
+    }
+
+    /**
+     * Estimates the outside of the viewport offset for the item. Used to understand from
+     * where to start animation for the item which wasn't visible previously or where it should
+     * end for the item which is not going to be visible in the end.
+     */
+    private fun calculateExpectedOffset(
+        index: Int,
+        mainAxisSizeWithSpacings: Int,
+        averageLineMainAxisSize: Int,
+        scrolledBy: IntOffset,
+        reverseLayout: Boolean,
+        mainAxisLayoutSize: Int,
+        fallback: Int
+    ): Int {
+        require(slotsPerLine != 0)
+        val beforeViewportStart =
+            if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+        val afterViewportEnd =
+            if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
+        return when {
+            beforeViewportStart -> {
+                val diff = ((index - viewportEndItemIndex).absoluteValue + slotsPerLine - 1) /
+                    slotsPerLine
+                mainAxisLayoutSize + viewportEndItemNotVisiblePartSize +
+                    averageLineMainAxisSize * (diff - 1) +
+                    scrolledBy.mainAxis
+            }
+            afterViewportEnd -> {
+                val diff = ((viewportStartItemIndex - index).absoluteValue + slotsPerLine - 1) /
+                    slotsPerLine
+                viewportStartItemNotVisiblePartSize - mainAxisSizeWithSpacings -
+                    averageLineMainAxisSize * (diff - 1) +
+                    scrolledBy.mainAxis
+            }
+            else -> {
+                fallback
+            }
+        }
+    }
+
+    private fun startAnimationsIfNeeded(item: TvLazyGridPositionedItem, itemInfo: ItemInfo) {
+        // first we make sure our item info is up to date (has the item placeables count)
+        while (itemInfo.placeables.size > item.placeablesCount) {
+            itemInfo.placeables.removeLast()
+        }
+        while (itemInfo.placeables.size < item.placeablesCount) {
+            val newPlaceableInfoIndex = itemInfo.placeables.size
+            val rawOffset = item.offset
+            itemInfo.placeables.add(
+                PlaceableInfo(
+                    rawOffset - itemInfo.notAnimatableDelta,
+                    item.getMainAxisSize(newPlaceableInfoIndex)
+                )
+            )
+        }
+
+        itemInfo.placeables.fastForEachIndexed { index, placeableInfo ->
+            val currentTarget = placeableInfo.targetOffset + itemInfo.notAnimatableDelta
+            val currentOffset = item.placeableOffset
+            placeableInfo.mainAxisSize = item.getMainAxisSize(index)
+            val animationSpec = item.getAnimationSpec(index)
+            if (currentTarget != currentOffset) {
+                placeableInfo.targetOffset = currentOffset - itemInfo.notAnimatableDelta
+                if (animationSpec != null) {
+                    placeableInfo.inProgress = true
+                    scope.launch {
+                        val finalSpec = if (placeableInfo.animatedOffset.isRunning) {
+                            // when interrupted, use the default spring, unless the spec is a spring.
+                            if (animationSpec is SpringSpec<IntOffset>) animationSpec else
+                                InterruptionSpec
+                        } else {
+                            animationSpec
+                        }
+
+                        try {
+                            placeableInfo.animatedOffset.animateTo(
+                                placeableInfo.targetOffset,
+                                finalSpec
+                            )
+                            placeableInfo.inProgress = false
+                        } catch (_: CancellationException) {
+                            // we don't reset inProgress in case of cancellation as it means
+                            // there is a new animation started which would reset it later
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun Int.toOffset() =
+        IntOffset(if (isVertical) 0 else this, if (!isVertical) 0 else this)
+
+    private val IntOffset.mainAxis get() = if (isVertical) y else x
+}
+
+private class ItemInfo(
+    var index: Int,
+    var crossAxisSize: Int,
+    var crossAxisOffset: Int
+) {
+    var notAnimatableDelta: IntOffset = IntOffset.Zero
+    val placeables = mutableListOf<PlaceableInfo>()
+}
+
+private class PlaceableInfo(initialOffset: IntOffset, var mainAxisSize: Int) {
+    val animatedOffset = Animatable(initialOffset, IntOffset.VectorConverter)
+    var targetOffset: IntOffset = initialOffset
+    var inProgress by mutableStateOf(false)
+}
+
+/**
+ * We switch to this spec when a duration based animation is being interrupted.
+ */
+private val InterruptionSpec = spring(
+    stiffness = Spring.StiffnessMediumLow,
+    visibilityThreshold = IntOffset.VisibilityThreshold
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
new file mode 100644
index 0000000..bfab189
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal interface LazyGridItemProvider : LazyLayoutItemProvider {
+    val spanLayoutProvider: LazyGridSpanLayoutProvider
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
new file mode 100644
index 0000000..9ee4e5e
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.Snapshot
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberItemProvider(
+    state: TvLazyGridState,
+    content: TvLazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
+    // of derivedState in return expr will only happen after the state value has been changed.
+    val nearestItemsRangeState = remember(state) {
+        mutableStateOf(
+            Snapshot.withoutReadObservation {
+                // State read is observed in composition, causing it to recompose 1 additional time.
+                calculateNearestItemsRange(state.firstVisibleItemIndex)
+            }
+        )
+    }
+    LaunchedEffect(nearestItemsRangeState) {
+        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
+            // MutableState's SnapshotMutationPolicy will make sure the provider is only
+            // recreated when the state is updated with a new range.
+            .collect { nearestItemsRangeState.value = it }
+    }
+    return remember(nearestItemsRangeState) {
+        LazyGridItemProviderImpl(
+            derivedStateOf {
+                val listScope = TvLazyGridScopeImpl().apply(latestContent.value)
+                LazyGridItemsSnapshot(
+                    listScope.intervals,
+                    listScope.hasCustomSpans,
+                    nearestItemsRangeState.value
+                )
+            }
+        )
+    }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal class LazyGridItemsSnapshot(
+    private val intervals: IntervalList<LazyGridIntervalContent>,
+    val hasCustomSpans: Boolean,
+    nearestItemsRange: IntRange
+) {
+    val itemsCount get() = intervals.size
+
+    val spanLayoutProvider = LazyGridSpanLayoutProvider(this)
+
+    fun getKey(index: Int): Any {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        val key = interval.value.key?.invoke(localIntervalIndex)
+        return key ?: getDefaultLazyLayoutKey(index)
+    }
+
+    fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return interval.value.span.invoke(this, localIntervalIndex)
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        interval.value.item.invoke(TvLazyGridItemScopeImpl, localIntervalIndex)
+    }
+
+    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
+
+    fun getContentType(index: Int): Any? {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return interval.value.type.invoke(localIntervalIndex)
+    }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal class LazyGridItemProviderImpl(
+    private val itemsSnapshot: State<LazyGridItemsSnapshot>
+) : LazyGridItemProvider {
+
+    override val itemCount get() = itemsSnapshot.value.itemsCount
+
+    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
+
+    @Composable
+    override fun Item(index: Int) {
+        itemsSnapshot.value.Item(index)
+    }
+
+    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
+
+    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
+
+    override val spanLayoutProvider: LazyGridSpanLayoutProvider
+        get() = itemsSnapshot.value.spanLayoutProvider
+}
+
+/**
+ * Traverses the interval [list] in order to create a mapping from the key to the index for all
+ * the indexes in the passed [range].
+ * The returned map will not contain the values for intervals with no key mapping provided.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal fun generateKeyToIndexMap(
+    range: IntRange,
+    list: IntervalList<LazyGridIntervalContent>
+): Map<Any, Int> {
+    val first = range.first
+    check(first >= 0)
+    val last = minOf(range.last, list.size - 1)
+    return if (last < first) {
+        emptyMap()
+    } else {
+        hashMapOf<Any, Int>().also { map ->
+            list.forEach(
+                fromIndex = first,
+                toIndex = last,
+            ) {
+                if (it.value.key != null) {
+                    val keyFactory = requireNotNull(it.value.key)
+                    val start = maxOf(first, it.startIndex)
+                    val end = minOf(last, it.startIndex + it.size - 1)
+                    for (i in start..end) {
+                        map[keyFactory(i - it.startIndex)] = i
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ */
+private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
+    val slidingWindowStart = VisibleItemsSlidingWindowSize *
+        (firstVisibleItem / VisibleItemsSlidingWindowSize)
+
+    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
+    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
+    return start until end
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private val VisibleItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private val ExtraItemsNearTheSlidingWindow = 200
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
new file mode 100644
index 0000000..13569c4
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastSumBy
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Measures and calculates the positions for the currently visible items. The result is produced
+ * as a [TvLazyGridMeasureResult] which contains all the calculations.
+ */
+internal fun measureLazyGrid(
+    itemsCount: Int,
+    measuredLineProvider: LazyMeasuredLineProvider,
+    measuredItemProvider: LazyMeasuredItemProvider,
+    mainAxisAvailableSize: Int,
+    slotsPerLine: Int,
+    beforeContentPadding: Int,
+    afterContentPadding: Int,
+    firstVisibleLineIndex: LineIndex,
+    firstVisibleLineScrollOffset: Int,
+    scrollToBeConsumed: Float,
+    constraints: Constraints,
+    isVertical: Boolean,
+    verticalArrangement: Arrangement.Vertical?,
+    horizontalArrangement: Arrangement.Horizontal?,
+    reverseLayout: Boolean,
+    density: Density,
+    placementAnimator: LazyGridItemPlacementAnimator,
+    layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): TvLazyGridMeasureResult {
+    require(beforeContentPadding >= 0)
+    require(afterContentPadding >= 0)
+    if (itemsCount <= 0) {
+        // empty data set. reset the current scroll and report zero size
+        return TvLazyGridMeasureResult(
+            firstVisibleLine = null,
+            firstVisibleLineScrollOffset = 0,
+            canScrollForward = false,
+            consumedScroll = 0f,
+            measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+            visibleItemsInfo = emptyList(),
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+            totalItemsCount = 0,
+            reverseLayout = reverseLayout,
+            orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+            afterContentPadding = afterContentPadding
+        )
+    } else {
+        var currentFirstLineIndex = firstVisibleLineIndex
+        var currentFirstLineScrollOffset = firstVisibleLineScrollOffset
+
+        // represents the real amount of scroll we applied as a result of this measure pass.
+        var scrollDelta = scrollToBeConsumed.roundToInt()
+
+        // applying the whole requested scroll offset. we will figure out if we can't consume
+        // all of it later
+        currentFirstLineScrollOffset -= scrollDelta
+
+        // if the current scroll offset is less than minimally possible
+        if (currentFirstLineIndex == LineIndex(0) && currentFirstLineScrollOffset < 0) {
+            scrollDelta += currentFirstLineScrollOffset
+            currentFirstLineScrollOffset = 0
+        }
+
+        // this will contain all the MeasuredItems representing the visible lines
+        val visibleLines = mutableListOf<LazyMeasuredLine>()
+
+        // include the start padding so we compose items in the padding area. before starting
+        // scrolling forward we would remove it back
+        currentFirstLineScrollOffset -= beforeContentPadding
+
+        // define min and max offsets (min offset currently includes beforeContentPadding)
+        val minOffset = -beforeContentPadding
+        val maxOffset = mainAxisAvailableSize
+
+        // we had scrolled backward or we compose items in the start padding area, which means
+        // items before current firstLineScrollOffset should be visible. compose them and update
+        // firstLineScrollOffset
+        while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
+            val previous = LineIndex(currentFirstLineIndex.value - 1)
+            val measuredLine = measuredLineProvider.getAndMeasure(previous)
+            visibleLines.add(0, measuredLine)
+            currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
+            currentFirstLineIndex = previous
+        }
+        // if we were scrolled backward, but there were not enough lines before. this means
+        // not the whole scroll was consumed
+        if (currentFirstLineScrollOffset < minOffset) {
+            scrollDelta += currentFirstLineScrollOffset
+            currentFirstLineScrollOffset = minOffset
+        }
+
+        // neutralize previously added start padding as we stopped filling the before content padding
+        currentFirstLineScrollOffset += beforeContentPadding
+
+        var index = currentFirstLineIndex
+        val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+        var currentMainAxisOffset = -currentFirstLineScrollOffset
+
+        // first we need to skip lines we already composed while composing backward
+        visibleLines.fastForEach {
+            index++
+            currentMainAxisOffset += it.mainAxisSizeWithSpacings
+        }
+
+        // then composing visible lines forward until we fill the whole viewport.
+        // we want to have at least one line in visibleItems even if in fact all the items are
+        // offscreen, this can happen if the content padding is larger than the available size.
+        while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
+            val measuredLine = measuredLineProvider.getAndMeasure(index)
+            if (measuredLine.isEmpty()) {
+                --index
+                break
+            }
+
+            currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
+            if (currentMainAxisOffset <= minOffset &&
+                measuredLine.items.last().index.value != itemsCount - 1) {
+                // this line is offscreen and will not be placed. advance firstVisibleLineIndex
+                currentFirstLineIndex = index + 1
+                currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
+            } else {
+                visibleLines.add(measuredLine)
+            }
+            index++
+        }
+
+        // we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
+        // lets try to scroll back if we have enough lines before firstVisibleLineIndex.
+        if (currentMainAxisOffset < maxOffset) {
+            val toScrollBack = maxOffset - currentMainAxisOffset
+            currentFirstLineScrollOffset -= toScrollBack
+            currentMainAxisOffset += toScrollBack
+            while (currentFirstLineScrollOffset < beforeContentPadding &&
+                currentFirstLineIndex > LineIndex(0)
+            ) {
+                val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
+                val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
+                visibleLines.add(0, measuredLine)
+                currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
+                currentFirstLineIndex = previousIndex
+            }
+            scrollDelta += toScrollBack
+            if (currentFirstLineScrollOffset < 0) {
+                scrollDelta += currentFirstLineScrollOffset
+                currentMainAxisOffset += currentFirstLineScrollOffset
+                currentFirstLineScrollOffset = 0
+            }
+        }
+
+        // report the amount of pixels we consumed. scrollDelta can be smaller than
+        // scrollToBeConsumed if there were not enough lines to fill the offered space or it
+        // can be larger if lines were resized, or if, for example, we were previously
+        // displaying the line 15, but now we have only 10 lines in total in the data set.
+        val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+            abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+        ) {
+            scrollDelta.toFloat()
+        } else {
+            scrollToBeConsumed
+        }
+
+        // the initial offset for lines from visibleLines list
+        val visibleLinesScrollOffset = -currentFirstLineScrollOffset
+        var firstLine = visibleLines.first()
+
+        // even if we compose lines to fill before content padding we should ignore lines fully
+        // located there for the state's scroll position calculation (first line + first offset)
+        if (beforeContentPadding > 0) {
+            for (i in visibleLines.indices) {
+                val size = visibleLines[i].mainAxisSizeWithSpacings
+                if (currentFirstLineScrollOffset != 0 && size <= currentFirstLineScrollOffset &&
+                    i != visibleLines.lastIndex) {
+                    currentFirstLineScrollOffset -= size
+                    firstLine = visibleLines[i + 1]
+                } else {
+                    break
+                }
+            }
+        }
+
+        val layoutWidth = if (isVertical) {
+            constraints.maxWidth
+        } else {
+            constraints.constrainWidth(currentMainAxisOffset)
+        }
+        val layoutHeight = if (isVertical) {
+            constraints.constrainHeight(currentMainAxisOffset)
+        } else {
+            constraints.maxHeight
+        }
+
+        val positionedItems = calculateItemsOffsets(
+            lines = visibleLines,
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            finalMainAxisOffset = currentMainAxisOffset,
+            maxOffset = maxOffset,
+            firstLineScrollOffset = visibleLinesScrollOffset,
+            isVertical = isVertical,
+            verticalArrangement = verticalArrangement,
+            horizontalArrangement = horizontalArrangement,
+            reverseLayout = reverseLayout,
+            density = density
+        )
+
+        placementAnimator.onMeasured(
+            consumedScroll = consumedScroll.toInt(),
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            slotsPerLine = slotsPerLine,
+            reverseLayout = reverseLayout,
+            positionedItems = positionedItems,
+            measuredItemProvider = measuredItemProvider
+        )
+
+        return TvLazyGridMeasureResult(
+            firstVisibleLine = firstLine,
+            firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
+            canScrollForward = currentMainAxisOffset > maxOffset,
+            consumedScroll = consumedScroll,
+            measureResult = layout(layoutWidth, layoutHeight) {
+                positionedItems.fastForEach { it.place(this) }
+            },
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+            visibleItemsInfo = positionedItems,
+            totalItemsCount = itemsCount,
+            reverseLayout = reverseLayout,
+            orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+            afterContentPadding = afterContentPadding
+        )
+    }
+}
+
+/**
+ * Calculates [LazyMeasuredLine]s offsets.
+ */
+private fun calculateItemsOffsets(
+    lines: List<LazyMeasuredLine>,
+    layoutWidth: Int,
+    layoutHeight: Int,
+    finalMainAxisOffset: Int,
+    maxOffset: Int,
+    firstLineScrollOffset: Int,
+    isVertical: Boolean,
+    verticalArrangement: Arrangement.Vertical?,
+    horizontalArrangement: Arrangement.Horizontal?,
+    reverseLayout: Boolean,
+    density: Density,
+): MutableList<TvLazyGridPositionedItem> {
+    val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+    val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
+    if (hasSpareSpace) {
+        check(firstLineScrollOffset == 0)
+    }
+
+    val positionedItems = ArrayList<TvLazyGridPositionedItem>(lines.fastSumBy { it.items.size })
+
+    if (hasSpareSpace) {
+        val linesCount = lines.size
+        fun Int.reverseAware() =
+            if (!reverseLayout) this else linesCount - this - 1
+
+        val sizes = IntArray(linesCount) { index ->
+            lines[index.reverseAware()].mainAxisSize
+        }
+        val offsets = IntArray(linesCount) { 0 }
+        if (isVertical) {
+            with(requireNotNull(verticalArrangement)) {
+                density.arrange(mainAxisLayoutSize, sizes, offsets)
+            }
+        } else {
+            with(requireNotNull(horizontalArrangement)) {
+                // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+                density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+            }
+        }
+
+        val reverseAwareOffsetIndices =
+            if (reverseLayout) offsets.indices.reversed() else offsets.indices
+
+        for (index in reverseAwareOffsetIndices) {
+            val absoluteOffset = offsets[index]
+            // when reverseLayout == true, offsets are stored in the reversed order to items
+            val line = lines[index.reverseAware()]
+            val relativeOffset = if (reverseLayout) {
+                // inverse offset to align with scroll direction for positioning
+                mainAxisLayoutSize - absoluteOffset - line.mainAxisSize
+            } else {
+                absoluteOffset
+            }
+            positionedItems.addAll(
+                line.position(relativeOffset, layoutWidth, layoutHeight)
+            )
+        }
+    } else {
+        var currentMainAxis = firstLineScrollOffset
+        lines.fastForEach {
+            positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += it.mainAxisSizeWithSpacings
+        }
+    }
+    return positionedItems
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
new file mode 100644
index 0000000..c7a6165
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible item index and the first
+ * visible item scroll offset.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridScrollPosition(
+    initialIndex: Int = 0,
+    initialScrollOffset: Int = 0
+) {
+    var index by mutableStateOf(ItemIndex(initialIndex))
+        private set
+
+    var scrollOffset by mutableStateOf(initialScrollOffset)
+        private set
+
+    private var hadFirstNotEmptyLayout = false
+
+    /** The last known key of the first item at [index] line. */
+    private var lastKnownFirstItemKey: Any? = null
+
+    /**
+     * Updates the current scroll position based on the results of the last measurement.
+     */
+    fun updateFromMeasureResult(measureResult: TvLazyGridMeasureResult) {
+        lastKnownFirstItemKey = measureResult.firstVisibleLine?.items?.firstOrNull()?.key
+        // we ignore the index and offset from measureResult until we get at least one
+        // measurement with real items. otherwise the initial index and scroll passed to the
+        // state would be lost and overridden with zeros.
+        if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
+            hadFirstNotEmptyLayout = true
+            val scrollOffset = measureResult.firstVisibleLineScrollOffset
+            check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+            Snapshot.withoutReadObservation {
+                update(
+                    ItemIndex(
+                        measureResult.firstVisibleLine?.items?.firstOrNull()?.index?.value ?: 0
+                    ),
+                    scrollOffset
+                )
+            }
+        }
+    }
+
+    /**
+     * Updates the scroll position - the passed values will be used as a start position for
+     * composing the items during the next measure pass and will be updated by the real
+     * position calculated during the measurement. This means that there is guarantee that
+     * exactly this index and offset will be applied as it is possible that:
+     * a) there will be no item at this index in reality
+     * b) item at this index will be smaller than the asked scrollOffset, which means we would
+     * switch to the next item
+     * c) there will be not enough items to fill the viewport after the requested index, so we
+     * would have to compose few elements before the asked index, changing the first visible item.
+     */
+    fun requestPosition(index: ItemIndex, scrollOffset: Int) {
+        update(index, scrollOffset)
+        // clear the stored key as we have a direct request to scroll to [index] position and the
+        // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+        lastKnownFirstItemKey = null
+    }
+
+    /**
+     * In addition to keeping the first visible item index we also store the key of this item.
+     * When the user provided custom keys for the items this mechanism allows us to detect when
+     * there were items added or removed before our current first visible item and keep this item
+     * as the first visible one even given that its index has been changed.
+     */
+    fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+        Snapshot.withoutReadObservation {
+            update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+        }
+    }
+
+    private fun update(index: ItemIndex, scrollOffset: Int) {
+        require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+        if (index != this.index) {
+            this.index = index
+        }
+        if (scrollOffset != this.scrollOffset) {
+            this.scrollOffset = scrollOffset
+        }
+    }
+
+    private companion object {
+        /**
+         * Finds a position of the item with the given key in the grid. This logic allows us to
+         * detect when there were items added or removed before our current first item.
+         */
+        private fun findLazyGridIndexByKey(
+            key: Any?,
+            lastKnownIndex: ItemIndex,
+            itemProvider: LazyGridItemProvider
+        ): ItemIndex {
+            if (key == null) {
+                // there were no real item during the previous measure
+                return lastKnownIndex
+            }
+            if (lastKnownIndex.value < itemProvider.itemCount &&
+                key == itemProvider.getKey(lastKnownIndex.value)
+            ) {
+                // this item is still at the same index
+                return lastKnownIndex
+            }
+            val newIndex = itemProvider.keyToIndexMap[key]
+            if (newIndex != null) {
+                return ItemIndex(newIndex)
+            }
+            // fallback to the previous index if we don't know the new index of the item
+            return lastKnownIndex
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
new file mode 100644
index 0000000..20bbd68
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.copy
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.math.abs
+import kotlin.math.max
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+private class ItemFoundInScroll(
+    val item: TvLazyGridItemInfo,
+    val previousAnimation: AnimationState<Float, AnimationVector1D>
+) : CancellationException()
+
+private val TargetDistance = 2500.dp
+private val BoundDistance = 1500.dp
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("LazyGridScrolling: ${generateMsg()}")
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun TvLazyGridState.doSmoothScrollToItem(
+    index: Int,
+    scrollOffset: Int,
+    slotsPerLine: Int
+) {
+    require(index >= 0f) { "Index should be non-negative ($index)" }
+    fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
+        it.index == index
+    }
+    scroll {
+        try {
+            val targetDistancePx = with(density) { TargetDistance.toPx() }
+            val boundDistancePx = with(density) { BoundDistance.toPx() }
+            var loop = true
+            var anim = AnimationState(0f)
+            val targetItemInitialInfo = getTargetItem()
+            if (targetItemInitialInfo != null) {
+                // It's already visible, just animate directly
+                throw ItemFoundInScroll(
+                    targetItemInitialInfo,
+                    anim
+                )
+            }
+            val forward = index > firstVisibleItemIndex
+
+            fun isOvershot(): Boolean {
+                // Did we scroll past the item?
+                @Suppress("RedundantIf") // It's way easier to understand the logic this way
+                return if (forward) {
+                    if (firstVisibleItemIndex > index) {
+                        true
+                    } else if (
+                        firstVisibleItemIndex == index &&
+                        firstVisibleItemScrollOffset > scrollOffset
+                    ) {
+                        true
+                    } else {
+                        false
+                    }
+                } else { // backward
+                    if (firstVisibleItemIndex < index) {
+                        true
+                    } else if (
+                        firstVisibleItemIndex == index &&
+                        firstVisibleItemScrollOffset < scrollOffset
+                    ) {
+                        true
+                    } else {
+                        false
+                    }
+                }
+            }
+
+            var loops = 1
+            while (loop && layoutInfo.totalItemsCount > 0) {
+                val visibleItems = layoutInfo.visibleItemsInfo
+                val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
+                    visibleItems,
+                    true // TODO(b/191238807)
+                )
+                val before = index < firstVisibleItemIndex
+                val linesDiff =
+                    (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+                        slotsPerLine
+
+                val expectedDistance = (averageLineMainAxisSize * linesDiff).toFloat() +
+                    scrollOffset - firstVisibleItemScrollOffset
+                val target = if (abs(expectedDistance) < targetDistancePx) {
+                    expectedDistance
+                } else {
+                    if (forward) targetDistancePx else -targetDistancePx
+                }
+
+                debugLog {
+                    "Scrolling to index=$index offset=$scrollOffset from " +
+                        "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
+                        "averageSize=$averageLineMainAxisSize and calculated target=$target"
+                }
+
+                anim = anim.copy(value = 0f)
+                var prevValue = 0f
+                anim.animateTo(
+                    target,
+                    sequentialAnimation = (anim.velocity != 0f)
+                ) {
+                    // If we haven't found the item yet, check if it's visible.
+                    var targetItem = getTargetItem()
+
+                    if (targetItem == null) {
+                        // Springs can overshoot their target, clamp to the desired range
+                        val coercedValue = if (target > 0) {
+                            value.coerceAtMost(target)
+                        } else {
+                            value.coerceAtLeast(target)
+                        }
+                        val delta = coercedValue - prevValue
+                        debugLog {
+                            "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
+                        }
+
+                        val consumed = scrollBy(delta)
+                        targetItem = getTargetItem()
+                        if (targetItem != null) {
+                            debugLog { "Found the item after performing scrollBy()" }
+                        } else if (!isOvershot()) {
+                            if (delta != consumed) {
+                                debugLog { "Hit end without finding the item" }
+                                cancelAnimation()
+                                loop = false
+                                return@animateTo
+                            }
+                            prevValue += delta
+                            if (forward) {
+                                if (value > boundDistancePx) {
+                                    debugLog { "Struck bound going forward" }
+                                    cancelAnimation()
+                                }
+                            } else {
+                                if (value < -boundDistancePx) {
+                                    debugLog { "Struck bound going backward" }
+                                    cancelAnimation()
+                                }
+                            }
+
+                            // Magic constants for teleportation chosen arbitrarily by experiment
+                            if (forward) {
+                                if (
+                                    loops >= 2 &&
+                                    index - layoutInfo.visibleItemsInfo.last().index > 200
+                                ) {
+                                    // Teleport
+                                    debugLog { "Teleport forward" }
+                                    snapToItemIndexInternal(index = index - 200, scrollOffset = 0)
+                                }
+                            } else {
+                                if (
+                                    loops >= 2 &&
+                                    layoutInfo.visibleItemsInfo.first().index - index > 100
+                                ) {
+                                    // Teleport
+                                    debugLog { "Teleport backward" }
+                                    snapToItemIndexInternal(index = index + 200, scrollOffset = 0)
+                                }
+                            }
+                        }
+                    }
+
+                    // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
+                    // the final position, there's no need to animate to it.
+                    if (isOvershot()) {
+                        debugLog { "Overshot" }
+                        snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+                        loop = false
+                        cancelAnimation()
+                        return@animateTo
+                    } else if (targetItem != null) {
+                        debugLog { "Found item" }
+                        throw ItemFoundInScroll(
+                            targetItem,
+                            anim
+                        )
+                    }
+                }
+
+                loops++
+            }
+        } catch (itemFound: ItemFoundInScroll) {
+            // We found it, animate to it
+            // Bring to the requested position - will be automatically stopped if not possible
+            val anim = itemFound.previousAnimation.copy(value = 0f)
+            // TODO(b/191238807)
+            val target = (itemFound.item.offset.y + scrollOffset).toFloat()
+            var prevValue = 0f
+            debugLog {
+                "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
+            }
+            anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
+                // Springs can overshoot their target, clamp to the desired range
+                val coercedValue = when {
+                    target > 0 -> {
+                        value.coerceAtMost(target)
+                    }
+                    target < 0 -> {
+                        value.coerceAtLeast(target)
+                    }
+                    else -> {
+                        debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
+                        0f
+                    }
+                }
+                val delta = coercedValue - prevValue
+                debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
+                val consumed = scrollBy(delta)
+                if (delta != consumed /* hit the end, stop */ ||
+                    coercedValue != value /* would have overshot, stop */
+                ) {
+                    cancelAnimation()
+                }
+                prevValue += delta
+            }
+            // Once we're finished the animation, snap to the exact position to account for
+            // rounding error (otherwise we tend to end up with the previous item scrolled the
+            // tiniest bit onscreen)
+            // TODO: prevent temporarily scrolling *past* the item
+            snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun calculateLineAverageMainAxisSize(
+    visibleItems: List<TvLazyGridItemInfo>,
+    isVertical: Boolean
+): Int {
+    val lineOf: (Int) -> Int = {
+        if (isVertical) visibleItems[it].row else visibleItems[it].column
+    }
+
+    var totalLinesMainAxisSize = 0
+    var linesCount = 0
+
+    var lineStartIndex = 0
+    while (lineStartIndex < visibleItems.size) {
+        val currentLine = lineOf(lineStartIndex)
+        if (currentLine == -1) {
+            // Filter out exiting items.
+            ++lineStartIndex
+            continue
+        }
+
+        var lineMainAxisSize = 0
+        var lineEndIndex = lineStartIndex
+        while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
+            lineMainAxisSize = max(
+                lineMainAxisSize,
+                if (isVertical) {
+                    visibleItems[lineEndIndex].size.height
+                } else {
+                    visibleItems[lineEndIndex].size.width
+                }
+            )
+            ++lineEndIndex
+        }
+
+        totalLinesMainAxisSize += lineMainAxisSize
+        ++linesCount
+
+        lineStartIndex = lineEndIndex
+    }
+
+    return totalLinesMainAxisSize / linesCount
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
new file mode 100644
index 0000000..73e06cc
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Immutable
+
+/**
+ * Represents the span of an item in a [TvLazyVerticalGrid].
+ */
+@Immutable
+@kotlin.jvm.JvmInline
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+value class TvGridItemSpan internal constructor(private val packedValue: Long) {
+    /**
+     * The span of the item on the current line. This will be the horizontal span for items of
+     * [TvLazyVerticalGrid].
+     */
+    @ExperimentalFoundationApi
+    val currentLineSpan: Int get() = packedValue.toInt()
+}
+
+/**
+ * Creates a [TvGridItemSpan] with a specified [currentLineSpan]. This will be the horizontal span
+ * for an item of a [TvLazyVerticalGrid].
+ */
+fun TvGridItemSpan(currentLineSpan: Int) = TvGridItemSpan(currentLineSpan.toLong())
+
+/**
+ * Scope of lambdas used to calculate the spans of items in lazy grids.
+ */
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridItemSpanScope {
+    /**
+     * The max current line (horizontal for vertical grids) the item can occupy, such that
+     * it will be positioned on the current line.
+     *
+     * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for the first cell in
+     * the line, 2 for the second cell, and 1 for the last one. If you return a span count larger
+     * than [maxCurrentLineSpan] this means we can't fit this cell into the current line, so the
+     * cell will be positioned on the next line.
+     */
+    val maxCurrentLineSpan: Int
+
+    /**
+     * The max line span (horizontal for vertical grids) an item can occupy. This will be the
+     * number of columns in vertical grids or the number of rows in horizontal grids.
+     *
+     * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for each cell.
+     */
+    val maxLineSpan: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
new file mode 100644
index 0000000..e72ee1e
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import kotlin.math.min
+import kotlin.math.sqrt
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+    class LineConfiguration(val firstItemIndex: Int, val spans: List<TvGridItemSpan>)
+
+    /** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
+    private val buckets = ArrayList<Bucket>().apply { add(Bucket(0)) }
+    /**
+     * The interval at each we will store the starting element of lines. These will be then
+     * used to calculate the layout of arbitrary lines, by starting from the closest
+     * known "bucket start". The smaller the bucketSize, the smaller cost for calculating layout
+     * of arbitrary lines but the higher memory usage for [buckets].
+     */
+    private val bucketSize get() = sqrt(1.0 * totalSize / slotsPerLine).toInt() + 1
+    /** Caches the last calculated line index, useful when scrolling in main axis direction. */
+    private var lastLineIndex = 0
+    /** Caches the starting item index on [lastLineIndex]. */
+    private var lastLineStartItemIndex = 0
+    /** Caches the span of [lastLineStartItemIndex], if this was already calculated. */
+    private var lastLineStartKnownSpan = 0
+    /**
+     * Caches a calculated bucket, this is useful when scrolling in reverse main axis
+     * direction. We cannot only keep the last element, as we would not know previous max span.
+     */
+    private var cachedBucketIndex = -1
+    /**
+     * Caches layout of [cachedBucketIndex], this is useful when scrolling in reverse main axis
+     * direction. We cannot only keep the last element, as we would not know previous max span.
+     */
+    private val cachedBucket = mutableListOf<Int>()
+    /**
+     * List of 1x1 spans if we do not have custom spans.
+     */
+    private var previousDefaultSpans = emptyList<TvGridItemSpan>()
+    private fun getDefaultSpans(currentSlotsPerLine: Int) =
+        if (currentSlotsPerLine == previousDefaultSpans.size) {
+            previousDefaultSpans
+        } else {
+            List(currentSlotsPerLine) { TvGridItemSpan(1) }.also { previousDefaultSpans = it }
+        }
+
+    val totalSize get() = itemsSnapshot.itemsCount
+
+    /** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
+    var slotsPerLine = 0
+        set(value) {
+            if (value != field) {
+                field = value
+                invalidateCache()
+            }
+        }
+
+    fun getLineConfiguration(lineIndex: Int): LineConfiguration {
+        if (!itemsSnapshot.hasCustomSpans) {
+            // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
+            val firstItemIndex = lineIndex * slotsPerLine
+            return LineConfiguration(
+                firstItemIndex,
+                getDefaultSpans(slotsPerLine.coerceAtMost(totalSize - firstItemIndex)
+                    .coerceAtLeast(0))
+            )
+        }
+
+        val bucketIndex = min(lineIndex / bucketSize, buckets.size - 1)
+        // We can calculate the items on the line from the closest cached bucket start item.
+        var currentLine = bucketIndex * bucketSize
+        var currentItemIndex = buckets[bucketIndex].firstItemIndex
+        var knownCurrentItemSpan = buckets[bucketIndex].firstItemKnownSpan
+        // ... but try using the more localised cached values.
+        if (lastLineIndex in currentLine..lineIndex) {
+            // The last calculated value is a better start point. Common when scrolling main axis.
+            currentLine = lastLineIndex
+            currentItemIndex = lastLineStartItemIndex
+            knownCurrentItemSpan = lastLineStartKnownSpan
+        } else if (bucketIndex == cachedBucketIndex &&
+            lineIndex - currentLine < cachedBucket.size
+        ) {
+            // It happens that the needed line start is fully cached. Common when scrolling in
+            // reverse main axis, as we decided to cacheThisBucket previously.
+            currentItemIndex = cachedBucket[lineIndex - currentLine]
+            currentLine = lineIndex
+            knownCurrentItemSpan = 0
+        }
+
+        val cacheThisBucket = currentLine % bucketSize == 0 &&
+            lineIndex - currentLine in 2 until bucketSize
+        if (cacheThisBucket) {
+            cachedBucketIndex = bucketIndex
+            cachedBucket.clear()
+        }
+
+        check(currentLine <= lineIndex)
+
+        while (currentLine < lineIndex && currentItemIndex < totalSize) {
+            if (cacheThisBucket) {
+                cachedBucket.add(currentItemIndex)
+            }
+
+            var spansUsed = 0
+            while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
+                val span = if (knownCurrentItemSpan == 0) {
+                    spanOf(currentItemIndex, slotsPerLine - spansUsed)
+                } else {
+                    knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
+                }
+                if (spansUsed + span > slotsPerLine) {
+                    knownCurrentItemSpan = span
+                    break
+                }
+
+                currentItemIndex++
+                spansUsed += span
+            }
+            ++currentLine
+            if (currentLine % bucketSize == 0 && currentItemIndex < totalSize) {
+                val currentLineBucket = currentLine / bucketSize
+                // This should happen, as otherwise this should have been used as starting point.
+                check(buckets.size == currentLineBucket)
+                buckets.add(Bucket(currentItemIndex, knownCurrentItemSpan))
+            }
+        }
+
+        lastLineIndex = lineIndex
+        lastLineStartItemIndex = currentItemIndex
+        lastLineStartKnownSpan = knownCurrentItemSpan
+
+        val firstItemIndex = currentItemIndex
+        val spans = mutableListOf<TvGridItemSpan>()
+
+        var spansUsed = 0
+        while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
+            val span = if (knownCurrentItemSpan == 0) {
+                spanOf(currentItemIndex, slotsPerLine - spansUsed)
+            } else {
+                knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
+            }
+            if (spansUsed + span > slotsPerLine) break
+
+            currentItemIndex++
+            spans.add(TvGridItemSpan(span))
+            spansUsed += span
+        }
+        return LineConfiguration(firstItemIndex, spans)
+    }
+
+    /**
+     * Calculate the line of index [itemIndex].
+     */
+    fun getLineIndexOfItem(itemIndex: Int): LineIndex {
+        if (totalSize <= 0) {
+            return LineIndex(0)
+        }
+        require(itemIndex < totalSize)
+        if (!itemsSnapshot.hasCustomSpans) {
+            return LineIndex(itemIndex / slotsPerLine)
+        }
+
+        val lowerBoundBucket = buckets.binarySearch { it.firstItemIndex - itemIndex }.let {
+            if (it >= 0) it else -it - 2
+        }
+        var currentLine = lowerBoundBucket * bucketSize
+        var currentItemIndex = buckets[lowerBoundBucket].firstItemIndex
+
+        require(currentItemIndex <= itemIndex)
+        var spansUsed = 0
+        while (currentItemIndex < itemIndex) {
+            val span = spanOf(currentItemIndex++, slotsPerLine - spansUsed)
+            if (spansUsed + span < slotsPerLine) {
+                spansUsed += span
+            } else if (spansUsed + span == slotsPerLine) {
+                ++currentLine
+                spansUsed = 0
+            } else {
+                // spansUsed + span > slotsPerLine
+                ++currentLine
+                spansUsed = span
+            }
+            if (currentLine % bucketSize == 0) {
+                val currentLineBucket = currentLine / bucketSize
+                if (currentLineBucket >= buckets.size) {
+                    buckets.add(Bucket(currentItemIndex - if (spansUsed > 0) 1 else 0))
+                }
+            }
+        }
+        if (spansUsed + spanOf(itemIndex, slotsPerLine - spansUsed) > slotsPerLine) {
+            ++currentLine
+        }
+
+        return LineIndex(currentLine)
+    }
+
+    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+        with(TvLazyGridItemSpanScopeImpl) {
+            maxCurrentLineSpan = maxSpan
+            maxLineSpan = slotsPerLine
+
+            getSpan(itemIndex).currentLineSpan.coerceIn(1, slotsPerLine)
+        }
+    }
+
+    private fun invalidateCache() {
+        buckets.clear()
+        buckets.add(Bucket(0))
+        lastLineIndex = 0
+        lastLineStartItemIndex = 0
+        cachedBucketIndex = -1
+        cachedBucket.clear()
+    }
+
+    private class Bucket(
+        /** Index of the first item in the bucket */
+        val firstItemIndex: Int,
+        /** Known span of the first item. Not zero only if this item caused "line break". */
+        val firstItemKnownSpan: Int = 0
+    )
+
+    private object TvLazyGridItemSpanScopeImpl : TvLazyGridItemSpanScope {
+        override var maxCurrentLineSpan = 0
+        override var maxLineSpan = 0
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt
new file mode 100644
index 0000000..9041d51e
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured item of the lazy grid. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItem(
+    val index: ItemIndex,
+    val key: Any,
+    private val isVertical: Boolean,
+    /**
+     * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
+     * [placeables] is empty.
+     */
+    val crossAxisSize: Int,
+    val mainAxisSpacing: Int,
+    private val reverseLayout: Boolean,
+    private val layoutDirection: LayoutDirection,
+    private val beforeContentPadding: Int,
+    private val afterContentPadding: Int,
+    val placeables: Array<Placeable>,
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    /**
+     * The offset which shouldn't affect any calculations but needs to be applied for the final
+     * value passed into the place() call.
+     */
+    private val visualOffset: IntOffset
+) {
+    /**
+     * Main axis size of the item - the max main axis size of the placeables.
+     */
+    val mainAxisSize: Int
+
+    /**
+     * The max main axis size of the placeables plus mainAxisSpacing.
+     */
+    val mainAxisSizeWithSpacings: Int
+
+    init {
+        var maxMainAxis = 0
+        placeables.forEach {
+            maxMainAxis = maxOf(maxMainAxis, if (isVertical) it.height else it.width)
+        }
+        mainAxisSize = maxMainAxis
+        mainAxisSizeWithSpacings = maxMainAxis + mainAxisSpacing
+    }
+
+    /**
+     * Calculates positions for the inner placeables at [rawCrossAxisOffset], [rawCrossAxisOffset].
+     * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
+     * up outside of the viewport (for example one item consist of 2 placeables, and the first one
+     * is not going to be visible, so we don't place it as an optimization, but place the second
+     * one). If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+     */
+    fun position(
+        rawMainAxisOffset: Int,
+        rawCrossAxisOffset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int,
+        row: Int,
+        column: Int,
+        lineMainAxisSize: Int
+    ): TvLazyGridPositionedItem {
+        val wrappers = mutableListOf<LazyGridPlaceableWrapper>()
+
+        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+        val mainAxisOffset = if (reverseLayout) {
+            mainAxisLayoutSize - rawMainAxisOffset - mainAxisSize
+        } else {
+            rawMainAxisOffset
+        }
+        val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
+        val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
+            crossAxisLayoutSize - rawCrossAxisOffset - crossAxisSize
+        } else {
+            rawCrossAxisOffset
+        }
+        val placeableOffset = if (isVertical) {
+            IntOffset(crossAxisOffset, mainAxisOffset)
+        } else {
+            IntOffset(mainAxisOffset, crossAxisOffset)
+        }
+
+        var placeableIndex = if (reverseLayout) placeables.lastIndex else 0
+        while (if (reverseLayout) placeableIndex >= 0 else placeableIndex < placeables.size) {
+            val it = placeables[placeableIndex]
+            val addIndex = if (reverseLayout) 0 else wrappers.size
+            wrappers.add(
+                addIndex,
+                LazyGridPlaceableWrapper(placeableOffset, it, placeables[placeableIndex].parentData)
+            )
+            if (reverseLayout) placeableIndex-- else placeableIndex++
+        }
+
+        return TvLazyGridPositionedItem(
+            offset = if (isVertical) {
+                IntOffset(rawCrossAxisOffset, rawMainAxisOffset)
+            } else {
+                IntOffset(rawMainAxisOffset, rawCrossAxisOffset)
+            },
+            placeableOffset = placeableOffset,
+            index = index.value,
+            key = key,
+            row = row,
+            column = column,
+            size = if (isVertical) {
+                IntSize(crossAxisSize, mainAxisSize)
+            } else {
+                IntSize(mainAxisSize, crossAxisSize)
+            },
+            lineMainAxisSize = lineMainAxisSize,
+            mainAxisSpacing = mainAxisSpacing,
+            minMainAxisOffset = -if (!reverseLayout) {
+                beforeContentPadding
+            } else {
+                afterContentPadding
+            },
+            maxMainAxisOffset = mainAxisLayoutSize +
+                if (!reverseLayout) afterContentPadding else beforeContentPadding,
+            isVertical = isVertical,
+            wrappers = wrappers,
+            placementAnimator = placementAnimator,
+            visualOffset = visualOffset
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridPositionedItem(
+    override val offset: IntOffset,
+    val placeableOffset: IntOffset,
+    override val index: Int,
+    override val key: Any,
+    override val row: Int,
+    override val column: Int,
+    override val size: IntSize,
+    val lineMainAxisSize: Int,
+    private val mainAxisSpacing: Int,
+    private val minMainAxisOffset: Int,
+    private val maxMainAxisOffset: Int,
+    private val isVertical: Boolean,
+    private val wrappers: List<LazyGridPlaceableWrapper>,
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    private val visualOffset: IntOffset
+) : TvLazyGridItemInfo {
+    val placeablesCount: Int get() = wrappers.size
+
+    val mainAxisSizeWithSpacings: Int get() =
+        mainAxisSpacing + if (isVertical) size.height else size.width
+
+    val lineMainAxisSizeWithSpacings: Int get() = mainAxisSpacing + lineMainAxisSize
+
+    fun getMainAxisSize(index: Int) = wrappers[index].placeable.mainAxisSize
+
+    fun getCrossAxisSize() = if (isVertical) size.width else size.height
+
+    fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y
+
+    @Suppress("UNCHECKED_CAST")
+    fun getAnimationSpec(index: Int) =
+        wrappers[index].parentData as? FiniteAnimationSpec<IntOffset>?
+
+    val hasAnimations = run {
+        repeat(placeablesCount) { index ->
+            if (getAnimationSpec(index) != null) {
+                return@run true
+            }
+        }
+        false
+    }
+
+    fun place(
+        scope: Placeable.PlacementScope,
+    ) = with(scope) {
+        repeat(placeablesCount) { index ->
+            val placeable = wrappers[index].placeable
+            val minOffset = minMainAxisOffset - placeable.mainAxisSize
+            val maxOffset = maxMainAxisOffset
+            val offset = if (getAnimationSpec(index) != null) {
+                placementAnimator.getAnimatedOffset(
+                    key, index, minOffset, maxOffset, placeableOffset
+                )
+            } else {
+                placeableOffset
+            }
+            if (offset.mainAxis > minOffset && offset.mainAxis < maxOffset) {
+                if (isVertical) {
+                    placeable.placeWithLayer(offset + visualOffset)
+                } else {
+                    placeable.placeRelativeWithLayer(offset + visualOffset)
+                }
+            }
+        }
+    }
+
+    private val IntOffset.mainAxis get() = if (isVertical) y else x
+    private val Placeable.mainAxisSize get() = if (isVertical) height else width
+}
+
+internal class LazyGridPlaceableWrapper(
+    val offset: IntOffset,
+    val placeable: Placeable,
+    val parentData: Any?
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt
new file mode 100644
index 0000000..5ba2016
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away the subcomposition from the measuring logic.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
+    private val itemProvider: LazyGridItemProvider,
+    private val measureScope: LazyLayoutMeasureScope,
+    private val defaultMainAxisSpacing: Int,
+    private val measuredItemFactory: MeasuredItemFactory
+) {
+    /**
+     * Used to subcompose individual items of lazy grids. Composed placeables will be measured
+     * with the provided [constraints] and wrapped into [LazyMeasuredItem].
+     */
+    fun getAndMeasure(
+        index: ItemIndex,
+        mainAxisSpacing: Int = defaultMainAxisSpacing,
+        constraints: Constraints
+    ): LazyMeasuredItem {
+        val key = itemProvider.getKey(index.value)
+        val placeables = measureScope.measure(index.value, constraints)
+        val crossAxisSize = if (constraints.hasFixedWidth) {
+            constraints.minWidth
+        } else {
+            require(constraints.hasFixedHeight)
+            constraints.minHeight
+        }
+        return measuredItemFactory.createItem(
+            index,
+            key,
+            crossAxisSize,
+            mainAxisSpacing,
+            placeables
+        )
+    }
+
+    /**
+     * Contains the mapping between the key and the index. It could contain not all the items of
+     * the list as an optimization.
+     **/
+    val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+internal fun interface MeasuredItemFactory {
+    fun createItem(
+        index: ItemIndex,
+        key: Any,
+        crossAxisSize: Int,
+        mainAxisSpacing: Int,
+        placeables: Array<Placeable>
+    ): LazyMeasuredItem
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt
new file mode 100644
index 0000000..5310da9
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured line of the lazy list. Each item on the line can in fact consist of
+ * multiple placeables if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredLine constructor(
+    val index: LineIndex,
+    val items: Array<LazyMeasuredItem>,
+    private val spans: List<TvGridItemSpan>,
+    private val isVertical: Boolean,
+    private val slotsPerLine: Int,
+    private val layoutDirection: LayoutDirection,
+    /**
+     * Spacing to be added after [mainAxisSize], in the main axis direction.
+     */
+    private val mainAxisSpacing: Int,
+    private val crossAxisSpacing: Int
+) {
+    /**
+     * Main axis size of the line - the max main axis size of the items on the line.
+     */
+    val mainAxisSize: Int
+
+    /**
+     * Sum of [mainAxisSpacing] and the max of the main axis sizes of the placeables on the line.
+     */
+    val mainAxisSizeWithSpacings: Int
+
+    init {
+        var maxMainAxis = 0
+        items.forEach { item ->
+            maxMainAxis = maxOf(maxMainAxis, item.mainAxisSize)
+        }
+        mainAxisSize = maxMainAxis
+        mainAxisSizeWithSpacings = mainAxisSize + mainAxisSpacing
+    }
+
+    /**
+     * Whether this line contains any items.
+     */
+    fun isEmpty() = items.isEmpty()
+
+    /**
+     * Calculates positions for the [items] at [offset] main axis position.
+     * If [reverseOrder] is true the [items] would be placed in the inverted order.
+     */
+    fun position(
+        offset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ): List<TvLazyGridPositionedItem> {
+        var usedCrossAxis = 0
+        var usedSpan = 0
+        return items.mapIndexed { itemIndex, item ->
+            val span = spans[itemIndex].currentLineSpan
+            val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
+                slotsPerLine - usedSpan - span
+            } else {
+                usedSpan
+            }
+
+            item.position(
+                rawMainAxisOffset = offset,
+                rawCrossAxisOffset = usedCrossAxis,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                row = if (isVertical) index.value else startSlot,
+                column = if (isVertical) startSlot else index.value,
+                lineMainAxisSize = mainAxisSize
+            ).also {
+                usedCrossAxis += item.crossAxisSize + crossAxisSpacing
+                usedSpan += span
+            }
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
new file mode 100644
index 0000000..5cf56f4
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away subcomposition and span calculation from the measuring logic of entire lines.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredLineProvider(
+    private val isVertical: Boolean,
+    slotSizesSums: List<Int>,
+    crossAxisSpacing: Int,
+    private val gridItemsCount: Int,
+    private val spaceBetweenLines: Int,
+    private val measuredItemProvider: LazyMeasuredItemProvider,
+    private val spanLayoutProvider: LazyGridSpanLayoutProvider,
+    private val measuredLineFactory: MeasuredLineFactory
+) {
+    // The constraints for cross axis size. The main axis is not restricted.
+    internal val childConstraints: (startSlot: Int, span: Int) -> Constraints = { startSlot, span ->
+        val lastSlotSum = slotSizesSums[startSlot + span - 1]
+        val prevSlotSum = if (startSlot == 0) 0 else slotSizesSums[startSlot - 1]
+        val slotsSize = lastSlotSum - prevSlotSum
+        val crossAxisSize = slotsSize + crossAxisSpacing * (span - 1)
+        if (isVertical) {
+            Constraints.fixedWidth(crossAxisSize)
+        } else {
+            Constraints.fixedHeight(crossAxisSize)
+        }
+    }
+
+    /**
+     * Used to subcompose items on lines of lazy grids. Composed placeables will be measured
+     * with the correct constraints and wrapped into [LazyMeasuredLine].
+     */
+    fun getAndMeasure(lineIndex: LineIndex): LazyMeasuredLine {
+        val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex.value)
+        val lineItemsCount = lineConfiguration.spans.size
+
+        // we add space between lines as an extra spacing for all lines apart from the last one
+        // so the lazy grid measuring logic will take it into account.
+        val mainAxisSpacing = if (lineItemsCount == 0 ||
+            lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount) {
+            0
+        } else {
+            spaceBetweenLines
+        }
+
+        var startSlot = 0
+        val items = Array(lineItemsCount) {
+            val span = lineConfiguration.spans[it].currentLineSpan
+            val constraints = childConstraints(startSlot, span)
+            measuredItemProvider.getAndMeasure(
+                ItemIndex(lineConfiguration.firstItemIndex + it),
+                mainAxisSpacing,
+                constraints
+            ).also { startSlot += span }
+        }
+        return measuredLineFactory.createLine(
+            lineIndex,
+            items,
+            lineConfiguration.spans,
+            mainAxisSpacing
+        )
+    }
+
+    /**
+     * Contains the mapping between the key and the index. It could contain not all the items of
+     * the list as an optimization.
+     **/
+    val keyToIndexMap: Map<Any, Int> get() = measuredItemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+@OptIn(ExperimentalFoundationApi::class)
+internal fun interface MeasuredLineFactory {
+    fun createLine(
+        index: LineIndex,
+        items: Array<LazyMeasuredItem>,
+        spans: List<TvGridItemSpan>,
+        mainAxisSpacing: Int
+    ): LazyMeasuredLine
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
new file mode 100644
index 0000000..ea19b91
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
+internal fun Modifier.lazyGridSemantics(
+    itemProvider: LazyGridItemProvider,
+    state: TvLazyGridState,
+    coroutineScope: CoroutineScope,
+    isVertical: Boolean,
+    reverseScrolling: Boolean,
+    userScrollEnabled: Boolean
+) = this.then(
+    remember(
+        itemProvider,
+        state,
+        isVertical,
+        reverseScrolling,
+        userScrollEnabled
+    ) {
+        val indexForKeyMapping: (Any) -> Int = { needle ->
+            val key = itemProvider::getKey
+            var result = -1
+            for (index in 0 until itemProvider.itemCount) {
+                if (key(index) == needle) {
+                    result = index
+                    break
+                }
+            }
+            result
+        }
+
+        val accessibilityScrollState = ScrollAxisRange(
+            value = {
+                // This is a simple way of representing the current position without
+                // needing any lazy items to be measured. It's good enough so far, because
+                // screen-readers care mostly about whether scroll position changed or not
+                // rather than the actual offset in pixels.
+                state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+            },
+            maxValue = {
+                if (state.canScrollForward) {
+                    // If we can scroll further, we don't know the end yet,
+                    // but it's upper bounded by #items + 1
+                    itemProvider.itemCount + 1f
+                } else {
+                    // If we can't scroll further, the current value is the max
+                    state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+                }
+            },
+            reverseScrolling = reverseScrolling
+        )
+
+        val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+            { x, y ->
+                val delta = if (isVertical) {
+                    y
+                } else {
+                    x
+                }
+                coroutineScope.launch {
+                    (state as ScrollableState).animateScrollBy(delta)
+                }
+                // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+                true
+            }
+        } else {
+            null
+        }
+
+        val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+            { index ->
+                require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+                    "Can't scroll to index $index, it is out of " +
+                        "bounds [0, ${state.layoutInfo.totalItemsCount})"
+                }
+                coroutineScope.launch {
+                    state.scrollToItem(index)
+                }
+                true
+            }
+        } else {
+            null
+        }
+
+        // TODO(popam): check if this is correct - it would be nice to provide correct columns here
+        val collectionInfo = CollectionInfo(rowCount = -1, columnCount = -1)
+
+        Modifier.semantics {
+            indexForKey(indexForKeyMapping)
+
+            if (isVertical) {
+                verticalScrollAxisRange = accessibilityScrollState
+            } else {
+                horizontalScrollAxisRange = accessibilityScrollState
+            }
+
+            if (scrollByAction != null) {
+                scrollBy(action = scrollByAction)
+            }
+
+            if (scrollToIndexAction != null) {
+                scrollToIndex(action = scrollToIndexAction)
+            }
+
+            this.collectionInfo = collectionInfo
+        }
+    }
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
new file mode 100644
index 0000000..2bcedda
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about an individual item in lazy grids like [TvLazyVerticalGrid].
+ *
+ * @see TvLazyGridLayoutInfo
+ */
+sealed interface TvLazyGridItemInfo {
+    /**
+     * The index of the item in the grid.
+     */
+    val index: Int
+
+    /**
+     * The key of the item which was passed to the item() or items() function.
+     */
+    val key: Any
+
+    /**
+     * The offset of the item in pixels. It is relative to the top start of the lazy grid container.
+     */
+    val offset: IntOffset
+
+    /**
+     * The row occupied by the top start point of the item.
+     * If this is unknown, for example while this item is animating to exit the viewport and is
+     * still visible, the value will be [UnknownRow].
+     */
+    val row: Int
+
+    /**
+     * The column occupied by the top start point of the item.
+     * If this is unknown, for example while this item is animating to exit the viewport and is
+     * still visible, the value will be [UnknownColumn].
+     */
+    val column: Int
+
+    /**
+     * The pixel size of the item. Note that if you emit multiple layouts in the composable
+     * slot for the item then this size will be calculated as the max of their sizes.
+     */
+    val size: IntSize
+
+    companion object {
+        /**
+         * Possible value for [row], when they are unknown. This can happen when the item is
+         * visible while animating to exit the viewport.
+         */
+        const val UnknownRow = -1
+        /**
+         * Possible value for [column], when they are unknown. This can happen when the item is
+         * visible while animating to exit the viewport.
+         */
+        const val UnknownColumn = -1
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
new file mode 100644
index 0000000..5fae2451
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntOffset
+
+/**
+ * Receiver scope being used by the item content parameter of [TvLazyVerticalGrid].
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@Stable
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridItemScope {
+    /**
+     * This modifier animates the item placement within the Lazy grid.
+     *
+     * When you provide a key via [TvLazyGridScope.item]/[TvLazyGridScope.items] this modifier will
+     * enable item reordering animations. Aside from item reordering all other position changes
+     * caused by events like arrangement or alignment changes will also be animated.
+     *
+     * @param animationSpec a finite animation that will be used to animate the item placement.
+     */
+    @ExperimentalFoundationApi
+    fun Modifier.animateItemPlacement(
+        animationSpec: FiniteAnimationSpec<IntOffset> = spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = IntOffset.VisibilityThreshold
+        )
+    ): Modifier
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
new file mode 100644
index 0000000..35d063f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal object TvLazyGridItemScopeImpl : TvLazyGridItemScope {
+    @ExperimentalFoundationApi
+    override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
+        this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
+            name = "animateItemPlacement"
+            value = animationSpec
+        }))
+}
+
+private class AnimateItemPlacementModifier(
+    val animationSpec: FiniteAnimationSpec<IntOffset>,
+    inspectorInfo: InspectorInfo.() -> Unit,
+) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
+    override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AnimateItemPlacementModifier) return false
+        return animationSpec != other.animationSpec
+    }
+
+    override fun hashCode(): Int {
+        return animationSpec.hashCode()
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
new file mode 100644
index 0000000..6fc30dc
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy grids like
+ * [TvLazyVerticalGrid]. For example you can get the list of currently displayed items.
+ *
+ * Use [TvLazyGridState.layoutInfo] to retrieve this
+ */
+sealed interface TvLazyGridLayoutInfo {
+    /**
+     * The list of [TvLazyGridItemInfo] representing all the currently visible items.
+     */
+    val visibleItemsInfo: List<TvLazyGridItemInfo>
+
+    /**
+     * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
+     * which would be visible. Usually it is 0, but it can be negative if non-zero [beforeContentPadding]
+     * was applied as the content displayed in the content padding area is still visible.
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportStartOffset: Int
+
+    /**
+     * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
+     * which would be visible. It is the size of the lazy grid layout minus [beforeContentPadding].
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportEndOffset: Int
+
+    /**
+     * The total count of items passed to [TvLazyVerticalGrid].
+     */
+    val totalItemsCount: Int
+
+    /**
+     * The size of the viewport in pixels. It is the lazy grid layout size including all the
+     * content paddings.
+     */
+    val viewportSize: IntSize
+
+    /**
+     * The orientation of the lazy grid.
+     */
+    val orientation: Orientation
+
+    /**
+     * True if the direction of scrolling and layout is reversed.
+     */
+    val reverseLayout: Boolean
+
+    /**
+     * The content padding in pixels applied before the first row/column in the direction of scrolling.
+     * For example it is a top content padding for LazyVerticalGrid with reverseLayout set to false.
+     */
+    val beforeContentPadding: Int
+
+    /**
+     * The content padding in pixels applied after the last row/column in the direction of scrolling.
+     * For example it is a bottom content padding for LazyVerticalGrid with reverseLayout set to false.
+     */
+    val afterContentPadding: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
new file mode 100644
index 0000000..51bee04
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * The result of the measure pass for lazy list layout.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridMeasureResult(
+    // properties defining the scroll position:
+    /** The new first visible line of items.*/
+    val firstVisibleLine: LazyMeasuredLine?,
+    /** The new value for [TvLazyGridState.firstVisibleItemScrollOffset].*/
+    val firstVisibleLineScrollOffset: Int,
+    /** True if there is some space available to continue scrolling in the forward direction.*/
+    val canScrollForward: Boolean,
+    /** The amount of scroll consumed during the measure pass.*/
+    val consumedScroll: Float,
+    /** MeasureResult defining the layout.*/
+    measureResult: MeasureResult,
+    // properties representing the info needed for LazyListLayoutInfo:
+    /** see [TvLazyGridLayoutInfo.visibleItemsInfo] */
+    override val visibleItemsInfo: List<TvLazyGridItemInfo>,
+    /** see [TvLazyGridLayoutInfo.viewportStartOffset] */
+    override val viewportStartOffset: Int,
+    /** see [TvLazyGridLayoutInfo.viewportEndOffset] */
+    override val viewportEndOffset: Int,
+    /** see [TvLazyGridLayoutInfo.totalItemsCount] */
+    override val totalItemsCount: Int,
+    /** see [TvLazyGridLayoutInfo.reverseLayout] */
+    override val reverseLayout: Boolean,
+    /** see [TvLazyGridLayoutInfo.orientation] */
+    override val orientation: Orientation,
+    /** see [TvLazyGridLayoutInfo.afterContentPadding] */
+    override val afterContentPadding: Int
+) : TvLazyGridLayoutInfo, MeasureResult by measureResult {
+    override val viewportSize: IntSize
+        get() = IntSize(width, height)
+    override val beforeContentPadding: Int get() = -viewportStartOffset
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
new file mode 100644
index 0000000..bfd2510
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridScopeImpl : TvLazyGridScope {
+    internal val intervals = MutableIntervalList<LazyGridIntervalContent>()
+    internal var hasCustomSpans = false
+
+    private val DefaultSpan: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan = { TvGridItemSpan(1) }
+
+    override fun item(
+        key: Any?,
+        span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)?,
+        contentType: Any?,
+        content: @Composable TvLazyGridItemScope.() -> Unit
+    ) {
+        intervals.addInterval(
+            1,
+            LazyGridIntervalContent(
+                key = key?.let { { key } },
+                span = span?.let { { span() } } ?: DefaultSpan,
+                type = { contentType },
+                item = { content() }
+            )
+        )
+        if (span != null) hasCustomSpans = true
+    }
+
+    override fun items(
+        count: Int,
+        key: ((index: Int) -> Any)?,
+        span: (TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan)?,
+        contentType: (index: Int) -> Any?,
+        itemContent: @Composable TvLazyGridItemScope.(index: Int) -> Unit
+    ) {
+        intervals.addInterval(
+            count,
+            LazyGridIntervalContent(
+                key = key,
+                span = span ?: DefaultSpan,
+                type = contentType,
+                item = itemContent
+            )
+        )
+        if (span != null) hasCustomSpans = true
+    }
+}
+
+internal class LazyGridIntervalContent(
+    val key: ((index: Int) -> Any)?,
+    val span: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan,
+    val type: ((index: Int) -> Any?),
+    val item: @Composable TvLazyGridItemScope.(Int) -> Unit
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
new file mode 100644
index 0000000..ed5b2bc
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+/**
+ * DSL marker used to distinguish between lazy grid dsl scope and the item content scope.
+ */
+@DslMarker
+annotation class TvLazyGridScopeMarker
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
new file mode 100644
index 0000000..df4e4c2
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.list.AwaitFirstLayoutModifier
+import kotlin.math.abs
+
+/**
+ * Creates a [TvLazyGridState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [TvLazyGridState.firstVisibleItemScrollOffset]
+ */
+@Composable
+fun rememberTvLazyGridState(
+    initialFirstVisibleItemIndex: Int = 0,
+    initialFirstVisibleItemScrollOffset: Int = 0
+): TvLazyGridState {
+    return rememberSaveable(saver = TvLazyGridState.Saver) {
+        TvLazyGridState(
+            initialFirstVisibleItemIndex,
+            initialFirstVisibleItemScrollOffset
+        )
+    }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ *
+ * In most cases, this will be created via [rememberTvLazyGridState].
+ *
+ * @param firstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [TvLazyGridState.firstVisibleItemScrollOffset]
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+class TvLazyGridState constructor(
+    firstVisibleItemIndex: Int = 0,
+    firstVisibleItemScrollOffset: Int = 0
+) : ScrollableState {
+    /**
+     * The holder class for the current scroll position.
+     */
+    private val scrollPosition =
+        LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+
+    /**
+     * The index of the first item that is visible.
+     *
+     * Note that this property is observable and if you use it in the composable function it will
+     * be recomposed on every change causing potential performance issues.
+     */
+    val firstVisibleItemIndex: Int get() = scrollPosition.index.value
+
+    /**
+     * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the
+     * amount that the item is offset backwards
+     */
+    val firstVisibleItemScrollOffset: Int get() = scrollPosition.scrollOffset
+
+    /** Backing state for [layoutInfo] */
+    private val layoutInfoState = mutableStateOf<TvLazyGridLayoutInfo>(EmptyTvLazyGridLayoutInfo)
+
+    /**
+     * The object of [TvLazyGridLayoutInfo] calculated during the last layout pass. For example,
+     * you can use it to calculate what items are currently visible.
+     *
+     * Note that this property is observable and is updated after every scroll or remeasure.
+     * If you use it in the composable function it will be recomposed on every change causing
+     * potential performance issues including infinity recomposition loop.
+     * Therefore, avoid using it in the composition.
+     */
+    val layoutInfo: TvLazyGridLayoutInfo get() = layoutInfoState.value
+
+    /**
+     * [InteractionSource] that will be used to dispatch drag events when this
+     * grid is being dragged. If you want to know whether the fling (or animated scroll) is in
+     * progress, use [isScrollInProgress].
+     */
+    val interactionSource: InteractionSource get() = internalInteractionSource
+
+    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+    /**
+     * The amount of scroll to be consumed in the next layout pass.  Scrolling forward is negative
+     * - that is, it is the amount that the items are offset in y
+     */
+    internal var scrollToBeConsumed = 0f
+        private set
+
+    /**
+     * Needed for [animateScrollToItem]. Updated on every measure.
+     */
+    internal var slotsPerLine: Int by mutableStateOf(0)
+
+    /**
+     * Needed for [animateScrollToItem]. Updated on every measure.
+     */
+    internal var density: Density by mutableStateOf(Density(1f, 1f))
+
+    /**
+     * Needed for [notifyPrefetch].
+     */
+    internal var isVertical: Boolean by mutableStateOf(true)
+
+    /**
+     * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+     * we reached the end of the grid.
+     */
+    private val scrollableState = ScrollableState { -onScroll(-it) }
+
+    /**
+     * Only used for testing to confirm that we're not making too many measure passes
+     */
+    /*@VisibleForTesting*/
+    internal var numMeasurePasses: Int = 0
+        private set
+
+    /**
+     * Only used for testing to disable prefetching when needed to test the main logic.
+     */
+    /*@VisibleForTesting*/
+    internal var prefetchingEnabled: Boolean = true
+
+    /**
+     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+     */
+    private var lineToPrefetch = -1
+
+    /**
+     * The list of handles associated with the items from the [lineToPrefetch] line.
+     */
+    private var currentLinePrefetchHandles =
+        mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
+
+    /**
+     * Keeps the scrolling direction during the previous calculation in order to be able to
+     * detect the scrolling direction change.
+     */
+    private var wasScrollingForward = false
+
+    /**
+     * The [Remeasurement] object associated with our layout. It allows us to remeasure
+     * synchronously during scroll.
+     */
+    private var remeasurement: Remeasurement? by mutableStateOf(null)
+
+    /**
+     * The modifier which provides [remeasurement].
+     */
+    internal val remeasurementModifier = object : RemeasurementModifier {
+        override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+            this@TvLazyGridState.remeasurement = remeasurement
+        }
+    }
+
+    /**
+     * Provides a modifier which allows to delay some interactions (e.g. scroll)
+     * until layout is ready.
+     */
+    internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+    /**
+     * Finds items on a line and their measurement constraints. Used for prefetching.
+     */
+    internal var prefetchInfoRetriever: (line: LineIndex) -> List<Pair<Int, Constraints>> by
+    mutableStateOf({ emptyList() })
+
+    internal var placementAnimator by mutableStateOf<LazyGridItemPlacementAnimator?>(null)
+
+    /**
+     * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
+     * pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    suspend fun scrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        scrollOffset: Int = 0
+    ) {
+        scroll {
+            snapToItemIndexInternal(index, scrollOffset)
+        }
+    }
+
+    internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
+        scrollPosition.requestPosition(ItemIndex(index), scrollOffset)
+        // placement animation is not needed because we snap into a new position.
+        placementAnimator?.reset()
+        remeasurement?.forceRemeasure()
+    }
+
+    /**
+     * Call this function to take control of scrolling and gain the ability to send scroll events
+     * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
+     * performed within a [scroll] block (even if they don't call any other methods on this
+     * object) in order to guarantee that mutual exclusion is enforced.
+     *
+     * If [scroll] is called from elsewhere, this will be canceled.
+     */
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        awaitLayoutModifier.waitForFirstLayout()
+        scrollableState.scroll(scrollPriority, block)
+    }
+
+    override fun dispatchRawDelta(delta: Float): Float =
+        scrollableState.dispatchRawDelta(delta)
+
+    override val isScrollInProgress: Boolean
+        get() = scrollableState.isScrollInProgress
+
+    private var canScrollBackward: Boolean = false
+    internal var canScrollForward: Boolean = false
+        private set
+
+    // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
+    //  fine-grained control over scrolling
+    /*@VisibleForTesting*/
+    internal fun onScroll(distance: Float): Float {
+        if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+            return 0f
+        }
+        check(abs(scrollToBeConsumed) <= 0.5f) {
+            "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+        }
+        scrollToBeConsumed += distance
+
+        // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+        // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+        // we have less than 0.5 pixels
+        if (abs(scrollToBeConsumed) > 0.5f) {
+            val preScrollToBeConsumed = scrollToBeConsumed
+            remeasurement?.forceRemeasure()
+            if (prefetchingEnabled) {
+                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+            }
+        }
+
+        // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+        if (abs(scrollToBeConsumed) <= 0.5f) {
+            // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+            // that we consumed the whole thing
+            return distance
+        } else {
+            val scrollConsumed = distance - scrollToBeConsumed
+            // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+            // nested scrolling)
+            scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+            return scrollConsumed
+        }
+    }
+
+    private fun notifyPrefetch(delta: Float) {
+        val prefetchState = prefetchState
+        if (!prefetchingEnabled) {
+            return
+        }
+        val info = layoutInfo
+        if (info.visibleItemsInfo.isNotEmpty()) {
+            // check(isActive)
+            val scrollingForward = delta < 0
+            val lineToPrefetch: Int
+            val closestNextItemToPrefetch: Int
+            if (scrollingForward) {
+                lineToPrefetch = 1 + info.visibleItemsInfo.last().let {
+                    if (isVertical) it.row else it.column
+                }
+                closestNextItemToPrefetch = info.visibleItemsInfo.last().index + 1
+            } else {
+                lineToPrefetch = -1 + info.visibleItemsInfo.first().let {
+                    if (isVertical) it.row else it.column
+                }
+                closestNextItemToPrefetch = info.visibleItemsInfo.first().index - 1
+            }
+            if (lineToPrefetch != this.lineToPrefetch &&
+                closestNextItemToPrefetch in 0 until info.totalItemsCount
+            ) {
+                if (wasScrollingForward != scrollingForward) {
+                    // the scrolling direction has been changed which means the last prefetched
+                    // is not going to be reached anytime soon so it is safer to dispose it.
+                    // if this line is already visible it is safe to call the method anyway
+                    // as it will be no-op
+                    currentLinePrefetchHandles.forEach { it.cancel() }
+                }
+                this.wasScrollingForward = scrollingForward
+                this.lineToPrefetch = lineToPrefetch
+                currentLinePrefetchHandles.clear()
+                prefetchInfoRetriever(LineIndex(lineToPrefetch)).fastForEach {
+                    currentLinePrefetchHandles.add(
+                        prefetchState.schedulePrefetch(it.first, it.second)
+                    )
+                }
+            }
+        }
+    }
+
+    internal val prefetchState = LazyLayoutPrefetchState()
+
+    /**
+     * Animate (smooth scroll) to the given item.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    suspend fun animateScrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        scrollOffset: Int = 0
+    ) {
+        doSmoothScrollToItem(index, scrollOffset, slotsPerLine)
+    }
+
+    /**
+     *  Updates the state with the new calculated scroll position and consumed scroll.
+     */
+    internal fun applyMeasureResult(result: TvLazyGridMeasureResult) {
+        scrollPosition.updateFromMeasureResult(result)
+        scrollToBeConsumed -= result.consumedScroll
+        layoutInfoState.value = result
+
+        canScrollForward = result.canScrollForward
+        canScrollBackward = (result.firstVisibleLine?.index?.value ?: 0) != 0 ||
+            result.firstVisibleLineScrollOffset != 0
+
+        numMeasurePasses++
+    }
+
+    /**
+     * When the user provided custom keys for the items we can try to detect when there were
+     * items added or removed before our current first visible item and keep this item
+     * as the first visible one even given that its index has been changed.
+     */
+    internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+        scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [TvLazyGridState].
+         */
+        val Saver: Saver<TvLazyGridState, *> = listSaver(
+            save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+            restore = {
+                TvLazyGridState(
+                    firstVisibleItemIndex = it[0],
+                    firstVisibleItemScrollOffset = it[1]
+                )
+            }
+        )
+    }
+}
+
+private object EmptyTvLazyGridLayoutInfo : TvLazyGridLayoutInfo {
+    override val visibleItemsInfo = emptyList<TvLazyGridItemInfo>()
+    override val viewportStartOffset = 0
+    override val viewportEndOffset = 0
+    override val totalItemsCount = 0
+    override val viewportSize = IntSize.Zero
+    override val orientation = Orientation.Vertical
+    override val reverseLayout = false
+    override val beforeContentPadding: Int = 0
+    override val afterContentPadding: Int = 0
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt
new file mode 100644
index 0000000..7480db2
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+/**
+ * Represents an index in the list of items of lazy layout.
+ */
+@Suppress("NOTHING_TO_INLINE")
+@kotlin.jvm.JvmInline
+internal value class DataIndex(val value: Int) {
+    inline operator fun inc(): DataIndex = DataIndex(value + 1)
+    inline operator fun dec(): DataIndex = DataIndex(value - 1)
+    inline operator fun plus(i: Int): DataIndex = DataIndex(value + i)
+    inline operator fun minus(i: Int): DataIndex = DataIndex(value - i)
+    inline operator fun minus(i: DataIndex): DataIndex = DataIndex(value - i.value)
+    inline operator fun compareTo(other: DataIndex): Int = value - other.value
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
new file mode 100644
index 0000000..64c697a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+
+/* Copied from
+ compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+  and modified */
+
+/**
+ * Receiver scope which is used by [TvLazyColumn] and [TvLazyRow].
+ */
+@TvLazyListScopeMarker
+sealed interface TvLazyListScope {
+    /**
+     * Adds a single item.
+     *
+     * @param key a stable and unique key representing the item. Using the same key
+     * for multiple items in the list is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the list will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param contentType the type of the content of this item. The item compositions of the same
+     * type could be reused more efficiently. Note that null is a valid type and items of such
+     * type will be considered compatible.
+     * @param content the content of the item
+     */
+    fun item(
+        key: Any? = null,
+        contentType: Any? = null,
+        content: @Composable TvLazyListItemScope.() -> Unit
+    )
+
+    /**
+     * Adds a [count] of items.
+     *
+     * @param count the items count
+     * @param key a factory of stable and unique keys representing the item. Using the same key
+     * for multiple items in the list is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the list will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param contentType a factory of the content types for the item. The item compositions of
+     * the same type could be reused more efficiently. Note that null is a valid type and items of such
+     * type will be considered compatible.
+     * @param itemContent the content displayed by a single item
+     */
+    fun items(
+        count: Int,
+        key: ((index: Int) -> Any)? = null,
+        contentType: (index: Int) -> Any? = { null },
+        itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
+    )
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.items(
+    items: List<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    noinline contentType: (item: T) -> Any? = { null },
+    crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(items[index]) } else null,
+    contentType = { index: Int -> contentType(items[index]) }
+) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.itemsIndexed(
+    items: List<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+    contentType = { index -> contentType(index, items[index]) }
+) {
+    itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.items(
+    items: Array<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    noinline contentType: (item: T) -> Any? = { null },
+    crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(items[index]) } else null,
+    contentType = { index: Int -> contentType(items[index]) }
+) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds an array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.itemsIndexed(
+    items: Array<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+    contentType = { index -> contentType(index, items[index]) }
+) {
+    itemContent(it, items[it])
+}
+
+/**
+ * The horizontally scrolling list that only composes and lays out the currently visible items.
+ * The [content] block defines a DSL which allows you to emit items of different types. For
+ * example you can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add
+ * a list of items.
+ *
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are
+ * laid out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means
+ * that row is scrolled to the end. Note that [reverseLayout] does not change the behavior of
+ * [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###].
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * [TvLazyListScope.item] to add a single item or [TvLazyListScope.items] to add a list of items.
+ */
+@Composable
+fun TvLazyRow(
+    modifier: Modifier = Modifier,
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
+    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    content: TvLazyListScope.() -> Unit
+) {
+    LazyList(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        verticalAlignment = verticalAlignment,
+        horizontalArrangement = horizontalArrangement,
+        isVertical = false,
+        reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
+        content = content,
+        pivotOffsets = pivotOffsets
+    )
+}
+
+/**
+ * The vertically scrolling list that only composes and lays out the currently visible items.
+ * The [content] block defines a DSL which allows you to emit items of different types. For
+ * example you can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add
+ * a list of items.
+ *
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are
+ * laid out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means
+ * that column is scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
+ * [verticalArrangement],
+ * e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321### (bottom).
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param horizontalAlignment the horizontal alignment applied to the items.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * [TvLazyListScope.item] to add a single item or [TvLazyListScope.items] to add a list of items.
+ */
+@Composable
+fun TvLazyColumn(
+    modifier: Modifier = Modifier,
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    content: TvLazyListScope.() -> Unit
+) {
+    LazyList(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        horizontalAlignment = horizontalAlignment,
+        verticalArrangement = verticalArrangement,
+        isVertical = true,
+        reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
+        content = content,
+        pivotOffsets = pivotOffsets
+    )
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
new file mode 100644
index 0000000..6ee45b7
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+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
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.tv.foundation.lazy.lazyListBeyondBoundsModifier
+import androidx.tv.foundation.lazy.lazyListPinningModifier
+import androidx.tv.foundation.marioScrollable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun LazyList(
+    /** Modifier to be applied for the inner layout */
+    modifier: Modifier,
+    /** State controlling the scroll position */
+    state: TvLazyListState,
+    /** The inner padding to be added for the whole content(not for each individual item) */
+    contentPadding: PaddingValues,
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean,
+    /** The layout orientation of the list */
+    isVertical: Boolean,
+    /** Whether scrolling via the user gestures is allowed. */
+    userScrollEnabled: Boolean,
+    /** offsets of child element within the parent and starting edge of the child from the pivot
+     * defined by the parentOffset. */
+    pivotOffsets: PivotOffsets,
+    /** The alignment to align items horizontally. Required when isVertical is true */
+    horizontalAlignment: Alignment.Horizontal? = null,
+    /** The vertical arrangement for items. Required when isVertical is true */
+    verticalArrangement: Arrangement.Vertical? = null,
+    /** The alignment to align items vertically. Required when isVertical is false */
+    verticalAlignment: Alignment.Vertical? = null,
+    /** The horizontal arrangement for items. Required when isVertical is false */
+    horizontalArrangement: Arrangement.Horizontal? = null,
+    /** The content of the list */
+    content: TvLazyListScope.() -> Unit
+) {
+    val itemProvider = rememberItemProvider(state, content)
+    val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
+    val scope = rememberCoroutineScope()
+    val placementAnimator = remember(state, isVertical) {
+        LazyListItemPlacementAnimator(scope, isVertical)
+    }
+    state.placementAnimator = placementAnimator
+
+    val measurePolicy = rememberLazyListMeasurePolicy(
+        itemProvider,
+        state,
+        beyondBoundsInfo,
+        contentPadding,
+        reverseLayout,
+        isVertical,
+        horizontalAlignment,
+        verticalAlignment,
+        horizontalArrangement,
+        verticalArrangement,
+        placementAnimator
+    )
+
+    ScrollPositionUpdater(itemProvider, state)
+
+    val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
+
+    LazyLayout(
+        modifier = modifier
+            .then(state.remeasurementModifier)
+            .then(state.awaitLayoutModifier)
+            .lazyListSemantics(
+                itemProvider = itemProvider,
+                state = state,
+                coroutineScope = scope,
+                isVertical = isVertical,
+                reverseScrolling = reverseLayout,
+                userScrollEnabled = userScrollEnabled
+            )
+            .clipScrollableContainer(orientation)
+            .lazyListBeyondBoundsModifier(state, beyondBoundsInfo, reverseLayout)
+            .lazyListPinningModifier(state, beyondBoundsInfo)
+            .marioScrollable(
+                orientation = orientation,
+                reverseDirection = run {
+                    // A finger moves with the content, not with the viewport. Therefore,
+                    // always reverse once to have "natural" gesture that goes reversed to layout
+                    var reverseDirection = !reverseLayout
+                    // But if rtl and horizontal, things move the other way around
+                    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+                    if (isRtl && !isVertical) {
+                        reverseDirection = !reverseDirection
+                    }
+                    reverseDirection
+                },
+                state = state,
+                enabled = userScrollEnabled,
+                pivotOffsets = pivotOffsets
+            ),
+        prefetchState = state.prefetchState,
+        measurePolicy = measurePolicy,
+        itemProvider = itemProvider
+    )
+}
+
+/** Extracted to minimize the recomposition scope */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+private fun ScrollPositionUpdater(
+    itemProvider: LazyListItemProvider,
+    state: TvLazyListState
+) {
+    if (itemProvider.itemCount > 0) {
+        state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+    }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+private fun rememberLazyListMeasurePolicy(
+    /** Items provider of the list. */
+    itemProvider: LazyListItemProvider,
+    /** The state of the list. */
+    state: TvLazyListState,
+    /** Keeps track of the number of items we measure and place that are beyond visible bounds. */
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    /** The inner padding to be added for the whole content(nor for each individual item) */
+    contentPadding: PaddingValues,
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean,
+    /** The layout orientation of the list */
+    isVertical: Boolean,
+    /** The alignment to align items horizontally. Required when isVertical is true */
+    horizontalAlignment: Alignment.Horizontal? = null,
+    /** The alignment to align items vertically. Required when isVertical is false */
+    verticalAlignment: Alignment.Vertical? = null,
+    /** The horizontal arrangement for items. Required when isVertical is false */
+    horizontalArrangement: Arrangement.Horizontal? = null,
+    /** The vertical arrangement for items. Required when isVertical is true */
+    verticalArrangement: Arrangement.Vertical? = null,
+    /** Item placement animator. Should be notified with the measuring result */
+    placementAnimator: LazyListItemPlacementAnimator
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+    state,
+    beyondBoundsInfo,
+    contentPadding,
+    reverseLayout,
+    isVertical,
+    horizontalAlignment,
+    verticalAlignment,
+    horizontalArrangement,
+    verticalArrangement,
+    placementAnimator
+) {
+    { containerConstraints ->
+        checkScrollableContainerConstraints(
+            containerConstraints,
+            if (isVertical) Orientation.Vertical else Orientation.Horizontal
+        )
+
+        // resolve content paddings
+        val startPadding =
+            if (isVertical) {
+                contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+            }
+
+        val endPadding =
+            if (isVertical) {
+                contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+            }
+        val topPadding = contentPadding.calculateTopPadding().roundToPx()
+        val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+        val totalVerticalPadding = topPadding + bottomPadding
+        val totalHorizontalPadding = startPadding + endPadding
+        val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+        val beforeContentPadding = when {
+            isVertical && !reverseLayout -> topPadding
+            isVertical && reverseLayout -> bottomPadding
+            !isVertical && !reverseLayout -> startPadding
+            else -> endPadding // !isVertical && reverseLayout
+        }
+        val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+        val contentConstraints =
+            containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+        state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+
+        // Update the state's cached Density
+        state.density = this
+
+        // this will update the scope used by the item composables
+        itemProvider.itemScope.maxWidth = contentConstraints.maxWidth.toDp()
+        itemProvider.itemScope.maxHeight = contentConstraints.maxHeight.toDp()
+
+        val spaceBetweenItemsDp = if (isVertical) {
+            requireNotNull(verticalArrangement).spacing
+        } else {
+            requireNotNull(horizontalArrangement).spacing
+        }
+        val spaceBetweenItems = spaceBetweenItemsDp.roundToPx()
+
+        val itemsCount = itemProvider.itemCount
+
+        // can be negative if the content padding is larger than the max size from constraints
+        val mainAxisAvailableSize = if (isVertical) {
+            containerConstraints.maxHeight - totalVerticalPadding
+        } else {
+            containerConstraints.maxWidth - totalHorizontalPadding
+        }
+        val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+            IntOffset(startPadding, topPadding)
+        } else {
+            // When layout is reversed and paddings together take >100% of the available space,
+            // layout size is coerced to 0 when positioning. To take that space into account,
+            // we offset start padding by negative space between paddings.
+            IntOffset(
+                if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+                if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+            )
+        }
+
+        val measuredItemProvider = LazyMeasuredItemProvider(
+            contentConstraints,
+            isVertical,
+            itemProvider,
+            this
+        ) { index, key, placeables ->
+            // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
+            // the lazy list measuring logic will take it into account.
+            val spacing = if (index.value == itemsCount - 1) 0 else spaceBetweenItems
+            LazyMeasuredItem(
+                index = index.value,
+                placeables = placeables,
+                isVertical = isVertical,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                layoutDirection = layoutDirection,
+                reverseLayout = reverseLayout,
+                beforeContentPadding = beforeContentPadding,
+                afterContentPadding = afterContentPadding,
+                spacing = spacing,
+                visualOffset = visualItemOffset,
+                key = key,
+                placementAnimator = placementAnimator
+            )
+        }
+        state.premeasureConstraints = measuredItemProvider.childConstraints
+
+        val firstVisibleItemIndex: DataIndex
+        val firstVisibleScrollOffset: Int
+        Snapshot.withoutReadObservation {
+            firstVisibleItemIndex = DataIndex(state.firstVisibleItemIndex)
+            firstVisibleScrollOffset = state.firstVisibleItemScrollOffset
+        }
+
+        measureLazyList(
+            itemsCount = itemsCount,
+            itemProvider = measuredItemProvider,
+            mainAxisAvailableSize = mainAxisAvailableSize,
+            beforeContentPadding = beforeContentPadding,
+            afterContentPadding = afterContentPadding,
+            firstVisibleItemIndex = firstVisibleItemIndex,
+            firstVisibleItemScrollOffset = firstVisibleScrollOffset,
+            scrollToBeConsumed = state.scrollToBeConsumed,
+            constraints = contentConstraints,
+            isVertical = isVertical,
+            headerIndexes = itemProvider.headerIndexes,
+            verticalArrangement = verticalArrangement,
+            horizontalArrangement = horizontalArrangement,
+            reverseLayout = reverseLayout,
+            density = this,
+            placementAnimator = placementAnimator,
+            beyondBoundsInfo = beyondBoundsInfo,
+            layout = { width, height, placement ->
+                layout(
+                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                    containerConstraints.constrainHeight(height + totalVerticalPadding),
+                    emptyMap(),
+                    placement
+                )
+            }
+        ).also { state.applyMeasureResult(it) }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
new file mode 100644
index 0000000..d20d6f5
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.ui.util.fastForEachIndexed
+
+/**
+ * This method finds the sticky header in composedItems list or composes the header item if needed.
+ *
+ * @param composedVisibleItems list of items already composed and expected to be visible. if the
+ * header wasn't in this list but is needed the header will be added as the first item in this list.
+ * @param itemProvider the provider so we can compose a header if it wasn't composed already
+ * @param headerIndexes list of indexes of headers. Must be sorted.
+ * @param beforeContentPadding the padding before the first item in the list
+ */
+internal fun findOrComposeLazyListHeader(
+    composedVisibleItems: MutableList<LazyListPositionedItem>,
+    itemProvider: LazyMeasuredItemProvider,
+    headerIndexes: List<Int>,
+    beforeContentPadding: Int,
+    layoutWidth: Int,
+    layoutHeight: Int,
+): LazyListPositionedItem? {
+    var currentHeaderOffset: Int = Int.MIN_VALUE
+    var nextHeaderOffset: Int = Int.MIN_VALUE
+
+    var currentHeaderListPosition = -1
+    var nextHeaderListPosition = -1
+    // we use visibleItemsInfo and not firstVisibleItemIndex as visibleItemsInfo list also
+    // contains all the items which are visible in the start content padding area
+    val firstVisible = composedVisibleItems.first().index
+    // find the header which can be displayed
+    for (index in headerIndexes.indices) {
+        if (headerIndexes[index] <= firstVisible) {
+            currentHeaderListPosition = headerIndexes[index]
+            nextHeaderListPosition = headerIndexes.getOrElse(index + 1) { -1 }
+        } else {
+            break
+        }
+    }
+
+    var indexInComposedVisibleItems = -1
+    composedVisibleItems.fastForEachIndexed { index, item ->
+        if (item.index == currentHeaderListPosition) {
+            indexInComposedVisibleItems = index
+            currentHeaderOffset = item.offset
+        } else {
+            if (item.index == nextHeaderListPosition) {
+                nextHeaderOffset = item.offset
+            }
+        }
+    }
+
+    if (currentHeaderListPosition == -1) {
+        // we have no headers needing special handling
+        return null
+    }
+
+    val measuredHeaderItem = itemProvider.getAndMeasure(DataIndex(currentHeaderListPosition))
+
+    var headerOffset = if (currentHeaderOffset != Int.MIN_VALUE) {
+        maxOf(-beforeContentPadding, currentHeaderOffset)
+    } else {
+        -beforeContentPadding
+    }
+    // if we have a next header overlapping with the current header, the next one will be
+    // pushing the current one away from the viewport.
+    if (nextHeaderOffset != Int.MIN_VALUE) {
+        headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size)
+    }
+
+    return measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight).also {
+        if (indexInComposedVisibleItems != -1) {
+            composedVisibleItems[indexInComposedVisibleItems] = it
+        } else {
+            composedVisibleItems.add(0, it)
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
new file mode 100644
index 0000000..317b454
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Handles the item placement animations when it is set via [LazyItemScope.animateItemPlacement].
+ *
+ * This class is responsible for detecting when item position changed, figuring our start/end
+ * offsets and starting the animations.
+ */
+internal class LazyListItemPlacementAnimator(
+    private val scope: CoroutineScope,
+    private val isVertical: Boolean
+) {
+    // state containing an animation and all relevant info for each item.
+    private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+
+    // snapshot of the key to index map used for the last measuring.
+    private var keyToIndexMap: Map<Any, Int> = emptyMap()
+
+    // keeps the first and the last items positioned in the viewport and their visible part sizes.
+    private var viewportStartItemIndex = -1
+    private var viewportStartItemNotVisiblePartSize = 0
+    private var viewportEndItemIndex = -1
+    private var viewportEndItemNotVisiblePartSize = 0
+
+    // stored to not allocate it every pass.
+    private val positionedKeys = mutableSetOf<Any>()
+
+    /**
+     * Should be called after the measuring so we can detect position changes and start animations.
+     *
+     * Note that this method can compose new item and add it into the [positionedItems] list.
+     */
+    fun onMeasured(
+        consumedScroll: Int,
+        layoutWidth: Int,
+        layoutHeight: Int,
+        reverseLayout: Boolean,
+        positionedItems: MutableList<LazyListPositionedItem>,
+        itemProvider: LazyMeasuredItemProvider,
+    ) {
+        if (!positionedItems.fastAny { it.hasAnimations }) {
+            // no animations specified - no work needed
+            reset()
+            return
+        }
+
+        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+
+        // the consumed scroll is considered as a delta we don't need to animate
+        val notAnimatableDelta = (if (reverseLayout) -consumedScroll else consumedScroll).toOffset()
+
+        val newFirstItem = positionedItems.first()
+        val newLastItem = positionedItems.last()
+
+        var totalItemsSize = 0
+        // update known indexes and calculate the average size
+        positionedItems.fastForEach { item ->
+            keyToItemInfoMap[item.key]?.index = item.index
+            totalItemsSize += item.sizeWithSpacings
+        }
+        val averageItemSize = totalItemsSize / positionedItems.size
+
+        positionedKeys.clear()
+        // iterate through the items which are visible (without animated offsets)
+        positionedItems.fastForEach { item ->
+            positionedKeys.add(item.key)
+            val itemInfo = keyToItemInfoMap[item.key]
+            if (itemInfo == null) {
+                // there is no state associated with this item yet
+                if (item.hasAnimations) {
+                    val newItemInfo = ItemInfo(item.index)
+                    val previousIndex = keyToIndexMap[item.key]
+                    val firstPlaceableOffset = item.getOffset(0)
+                    val firstPlaceableSize = item.getMainAxisSize(0)
+
+                    val targetFirstPlaceableOffsetMainAxis = if (previousIndex == null) {
+                        // it is a completely new item. no animation is needed
+                        firstPlaceableOffset.mainAxis
+                    } else {
+                        val fallback = if (!reverseLayout) {
+                            firstPlaceableOffset.mainAxis
+                        } else {
+                            firstPlaceableOffset.mainAxis - item.sizeWithSpacings +
+                                firstPlaceableSize
+                        }
+                        calculateExpectedOffset(
+                            index = previousIndex,
+                            sizeWithSpacings = item.sizeWithSpacings,
+                            averageItemsSize = averageItemSize,
+                            scrolledBy = notAnimatableDelta,
+                            fallback = fallback,
+                            reverseLayout = reverseLayout,
+                            mainAxisLayoutSize = mainAxisLayoutSize,
+                            visibleItems = positionedItems
+                        ) + if (reverseLayout) {
+                            item.size - firstPlaceableSize
+                        } else {
+                            0
+                        }
+                    }
+                    val targetFirstPlaceableOffset = if (isVertical) {
+                        firstPlaceableOffset.copy(y = targetFirstPlaceableOffsetMainAxis)
+                    } else {
+                        firstPlaceableOffset.copy(x = targetFirstPlaceableOffsetMainAxis)
+                    }
+
+                    // populate placeable info list
+                    repeat(item.placeablesCount) { placeableIndex ->
+                        val diffToFirstPlaceableOffset =
+                            item.getOffset(placeableIndex) - firstPlaceableOffset
+                        newItemInfo.placeables.add(
+                            PlaceableInfo(
+                                targetFirstPlaceableOffset + diffToFirstPlaceableOffset,
+                                item.getMainAxisSize(placeableIndex)
+                            )
+                        )
+                    }
+                    keyToItemInfoMap[item.key] = newItemInfo
+                    startAnimationsIfNeeded(item, newItemInfo)
+                }
+            } else {
+                if (item.hasAnimations) {
+                    // apply new not animatable offset
+                    itemInfo.notAnimatableDelta += notAnimatableDelta
+                    startAnimationsIfNeeded(item, itemInfo)
+                } else {
+                    // no animation, clean up if needed
+                    keyToItemInfoMap.remove(item.key)
+                }
+            }
+        }
+
+        // previously we were animating items which are visible in the end state so we had to
+        // compare the current state with the state used for the previous measuring.
+        // now we will animate disappearing items so the current state is their starting state
+        // so we can update current viewport start/end items
+        if (!reverseLayout) {
+            viewportStartItemIndex = newFirstItem.index
+            viewportStartItemNotVisiblePartSize = newFirstItem.offset
+            viewportEndItemIndex = newLastItem.index
+            viewportEndItemNotVisiblePartSize =
+                newLastItem.offset + newLastItem.sizeWithSpacings - mainAxisLayoutSize
+        } else {
+            viewportStartItemIndex = newLastItem.index
+            viewportStartItemNotVisiblePartSize =
+                mainAxisLayoutSize - newLastItem.offset - newLastItem.size
+            viewportEndItemIndex = newFirstItem.index
+            viewportEndItemNotVisiblePartSize =
+                -newFirstItem.offset + (newFirstItem.sizeWithSpacings - newFirstItem.size)
+        }
+
+        val iterator = keyToItemInfoMap.iterator()
+        while (iterator.hasNext()) {
+            val entry = iterator.next()
+            if (!positionedKeys.contains(entry.key)) {
+                // found an item which was in our map previously but is not a part of the
+                // positionedItems now
+                val itemInfo = entry.value
+                // apply new not animatable delta for this item
+                itemInfo.notAnimatableDelta += notAnimatableDelta
+
+                val index = itemProvider.keyToIndexMap[entry.key]
+
+                // whether at least one placeable is within the viewport bounds.
+                // this usually means that we will start animation for it right now
+                val withinBounds = itemInfo.placeables.fastAny {
+                    val currentTarget = it.targetOffset + itemInfo.notAnimatableDelta
+                    currentTarget.mainAxis + it.size > 0 &&
+                        currentTarget.mainAxis < mainAxisLayoutSize
+                }
+
+                // whether the animation associated with the item has been finished
+                val isFinished = !itemInfo.placeables.fastAny { it.inProgress }
+
+                if ((!withinBounds && isFinished) ||
+                    index == null ||
+                    itemInfo.placeables.isEmpty()
+                ) {
+                    iterator.remove()
+                } else {
+
+                    val measuredItem = itemProvider.getAndMeasure(DataIndex(index))
+
+                    // calculate the target offset for the animation.
+                    val absoluteTargetOffset = calculateExpectedOffset(
+                        index = index,
+                        sizeWithSpacings = measuredItem.sizeWithSpacings,
+                        averageItemsSize = averageItemSize,
+                        scrolledBy = notAnimatableDelta,
+                        fallback = mainAxisLayoutSize,
+                        reverseLayout = reverseLayout,
+                        mainAxisLayoutSize = mainAxisLayoutSize,
+                        visibleItems = positionedItems
+                    )
+                    val targetOffset = if (reverseLayout) {
+                        mainAxisLayoutSize - absoluteTargetOffset - measuredItem.size
+                    } else {
+                        absoluteTargetOffset
+                    }
+
+                    val item = measuredItem.position(targetOffset, layoutWidth, layoutHeight)
+                    positionedItems.add(item)
+                    startAnimationsIfNeeded(item, itemInfo)
+                }
+            }
+        }
+
+        keyToIndexMap = itemProvider.keyToIndexMap
+    }
+
+    /**
+     * Returns the current animated item placement offset. By calling it only during the layout
+     * phase we can skip doing remeasure on every animation frame.
+     */
+    fun getAnimatedOffset(
+        key: Any,
+        placeableIndex: Int,
+        minOffset: Int,
+        maxOffset: Int,
+        rawOffset: IntOffset
+    ): IntOffset {
+        val itemInfo = keyToItemInfoMap[key] ?: return rawOffset
+        val item = itemInfo.placeables[placeableIndex]
+        val currentValue = item.animatedOffset.value + itemInfo.notAnimatableDelta
+        val currentTarget = item.targetOffset + itemInfo.notAnimatableDelta
+
+        // cancel the animation if it is fully out of the bounds.
+        if (item.inProgress &&
+            ((currentTarget.mainAxis < minOffset && currentValue.mainAxis < minOffset) ||
+            (currentTarget.mainAxis > maxOffset && currentValue.mainAxis > maxOffset))
+        ) {
+            scope.launch {
+                item.animatedOffset.snapTo(item.targetOffset)
+                item.inProgress = false
+            }
+        }
+
+        return currentValue
+    }
+
+    /**
+     * Should be called when the animations are not needed for the next positions change,
+     * for example when we snap to a new position.
+     */
+    fun reset() {
+        keyToItemInfoMap.clear()
+        keyToIndexMap = emptyMap()
+        viewportStartItemIndex = -1
+        viewportStartItemNotVisiblePartSize = 0
+        viewportEndItemIndex = -1
+        viewportEndItemNotVisiblePartSize = 0
+    }
+
+    /**
+     * Estimates the outside of the viewport offset for the item. Used to understand from
+     * where to start animation for the item which wasn't visible previously or where it should
+     * end for the item which is not going to be visible in the end.
+     */
+    private fun calculateExpectedOffset(
+        index: Int,
+        sizeWithSpacings: Int,
+        averageItemsSize: Int,
+        scrolledBy: IntOffset,
+        reverseLayout: Boolean,
+        mainAxisLayoutSize: Int,
+        fallback: Int,
+        visibleItems: List<LazyListPositionedItem>
+    ): Int {
+        val afterViewportEnd =
+            if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+        val beforeViewportStart =
+            if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
+        return when {
+            afterViewportEnd -> {
+                var itemsSizes = 0
+                // add sizes of the items between the last visible one and this one.
+                val range = if (!reverseLayout) {
+                    viewportEndItemIndex + 1 until index
+                } else {
+                    index + 1 until viewportEndItemIndex
+                }
+                for (i in range) {
+                    itemsSizes += visibleItems.getItemSize(
+                        itemIndex = i,
+                        fallback = averageItemsSize
+                    )
+                }
+                mainAxisLayoutSize + viewportEndItemNotVisiblePartSize + itemsSizes +
+                    scrolledBy.mainAxis
+            }
+            beforeViewportStart -> {
+                // add the size of this item as we need the start offset of this item.
+                var itemsSizes = sizeWithSpacings
+                // add sizes of the items between the first visible one and this one.
+                val range = if (!reverseLayout) {
+                    index + 1 until viewportStartItemIndex
+                } else {
+                    viewportStartItemIndex + 1 until index
+                }
+                for (i in range) {
+                    itemsSizes += visibleItems.getItemSize(
+                        itemIndex = i,
+                        fallback = averageItemsSize
+                    )
+                }
+                viewportStartItemNotVisiblePartSize - itemsSizes + scrolledBy.mainAxis
+            }
+            else -> {
+                fallback
+            }
+        }
+    }
+
+    private fun List<LazyListPositionedItem>.getItemSize(itemIndex: Int, fallback: Int): Int {
+        if (isEmpty() || itemIndex < first().index || itemIndex > last().index) return fallback
+        if ((itemIndex - first().index) < (last().index - itemIndex)) {
+            for (index in indices) {
+                val item = get(index)
+                if (item.index == itemIndex) return item.sizeWithSpacings
+                if (item.index > itemIndex) break
+            }
+        } else {
+            for (index in lastIndex downTo 0) {
+                val item = get(index)
+                if (item.index == itemIndex) return item.sizeWithSpacings
+                if (item.index < itemIndex) break
+            }
+        }
+        return fallback
+    }
+
+    private fun startAnimationsIfNeeded(item: LazyListPositionedItem, itemInfo: ItemInfo) {
+        // first we make sure our item info is up to date (has the item placeables count)
+        while (itemInfo.placeables.size > item.placeablesCount) {
+            itemInfo.placeables.removeLast()
+        }
+        while (itemInfo.placeables.size < item.placeablesCount) {
+            val newPlaceableInfoIndex = itemInfo.placeables.size
+            val rawOffset = item.getOffset(newPlaceableInfoIndex)
+            itemInfo.placeables.add(
+                PlaceableInfo(
+                    rawOffset - itemInfo.notAnimatableDelta,
+                    item.getMainAxisSize(newPlaceableInfoIndex)
+                )
+            )
+        }
+
+        itemInfo.placeables.fastForEachIndexed { index, placeableInfo ->
+            val currentTarget = placeableInfo.targetOffset + itemInfo.notAnimatableDelta
+            val currentOffset = item.getOffset(index)
+            placeableInfo.size = item.getMainAxisSize(index)
+            val animationSpec = item.getAnimationSpec(index)
+            if (currentTarget != currentOffset) {
+                placeableInfo.targetOffset = currentOffset - itemInfo.notAnimatableDelta
+                if (animationSpec != null) {
+                    placeableInfo.inProgress = true
+                    scope.launch {
+                        val finalSpec = if (placeableInfo.animatedOffset.isRunning) {
+                            // when interrupted, use the default spring, unless the spec is a spring.
+                            if (animationSpec is SpringSpec<IntOffset>) animationSpec else
+                                InterruptionSpec
+                        } else {
+                            animationSpec
+                        }
+
+                        try {
+                            placeableInfo.animatedOffset.animateTo(
+                                placeableInfo.targetOffset,
+                                finalSpec
+                            )
+                            placeableInfo.inProgress = false
+                        } catch (_: CancellationException) {
+                            // we don't reset inProgress in case of cancellation as it means
+                            // there is a new animation started which would reset it later
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun Int.toOffset() =
+        IntOffset(if (isVertical) 0 else this, if (!isVertical) 0 else this)
+
+    private val IntOffset.mainAxis get() = if (isVertical) y else x
+}
+
+private class ItemInfo(var index: Int) {
+    var notAnimatableDelta: IntOffset = IntOffset.Zero
+    val placeables = mutableListOf<PlaceableInfo>()
+}
+
+private class PlaceableInfo(
+    initialOffset: IntOffset,
+    var size: Int
+) {
+    val animatedOffset = Animatable(initialOffset, IntOffset.VectorConverter)
+    var targetOffset: IntOffset = initialOffset
+    var inProgress by mutableStateOf(false)
+}
+
+/**
+ * We switch to this spec when a duration based animation is being interrupted.
+ */
+private val InterruptionSpec = spring(
+    stiffness = Spring.StiffnessMediumLow,
+    visibilityThreshold = IntOffset.VisibilityThreshold
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
new file mode 100644
index 0000000..7d4137f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal sealed interface LazyListItemProvider : LazyLayoutItemProvider {
+    /** The list of indexes of the sticky header items */
+    val headerIndexes: List<Int>
+    /** The scope used by the item content lambdas */
+    val itemScope: TvLazyListItemScopeImpl
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
new file mode 100644
index 0000000..1e9966d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.Snapshot
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberItemProvider(
+    state: TvLazyListState,
+    content: TvLazyListScope.() -> Unit
+): LazyListItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
+    // of derivedState in return expr will only happen after the state value has been changed.
+    val nearestItemsRangeState = remember(state) {
+        mutableStateOf(
+            Snapshot.withoutReadObservation {
+                // State read is observed in composition, causing it to recompose 1 additional time.
+                calculateNearestItemsRange(state.firstVisibleItemIndex)
+            }
+        )
+    }
+
+    LaunchedEffect(nearestItemsRangeState) {
+        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
+            // MutableState's SnapshotMutationPolicy will make sure the provider is only
+            // recreated when the state is updated with a new range.
+            .collect { nearestItemsRangeState.value = it }
+    }
+    return remember(nearestItemsRangeState) {
+        LazyListItemProviderImpl(
+            derivedStateOf {
+                val listScope = TvLazyListScopeImpl().apply(latestContent.value)
+                LazyListItemsSnapshot(
+                    listScope.intervals,
+                    listScope.headerIndexes,
+                    nearestItemsRangeState.value
+                )
+            }
+        )
+    }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal class LazyListItemsSnapshot(
+    private val intervals: IntervalList<LazyListIntervalContent>,
+    val headerIndexes: List<Int>,
+    nearestItemsRange: IntRange
+) {
+    val itemsCount get() = intervals.size
+
+    fun getKey(index: Int): Any {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        val key = interval.value.key?.invoke(localIntervalIndex)
+        return key ?: getDefaultLazyLayoutKey(index)
+    }
+
+    @Composable
+    fun Item(scope: TvLazyListItemScope, index: Int) {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        interval.value.item.invoke(scope, localIntervalIndex)
+    }
+
+    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
+
+    fun getContentType(index: Int): Any? {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return interval.value.type.invoke(localIntervalIndex)
+    }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal class LazyListItemProviderImpl(
+    private val itemsSnapshot: State<LazyListItemsSnapshot>
+) : LazyListItemProvider {
+
+    override val itemScope = TvLazyListItemScopeImpl()
+
+    override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
+
+    override val itemCount get() = itemsSnapshot.value.itemsCount
+
+    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
+
+    @Composable
+    override fun Item(index: Int) {
+        itemsSnapshot.value.Item(itemScope, index)
+    }
+
+    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
+
+    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
+}
+
+@ExperimentalFoundationApi
+internal fun generateKeyToIndexMap(
+    range: IntRange,
+    list: IntervalList<LazyListIntervalContent>
+): Map<Any, Int> {
+    val first = range.first
+    check(first >= 0)
+    val last = minOf(range.last, list.size - 1)
+    return if (last < first) {
+        emptyMap()
+    } else {
+        hashMapOf<Any, Int>().also { map ->
+            list.forEach(
+                fromIndex = first,
+                toIndex = last,
+            ) {
+                if (it.value.key != null) {
+                    val keyFactory = requireNotNull(it.value.key)
+                    val start = maxOf(first, it.startIndex)
+                    val end = minOf(last, it.startIndex + it.size - 1)
+                    for (i in start..end) {
+                        map[keyFactory(i - it.startIndex)] = i
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ */
+private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
+    val slidingWindowStart = VisibleItemsSlidingWindowSize *
+        (firstVisibleItem / VisibleItemsSlidingWindowSize)
+
+    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
+    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
+    return start until end
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private val VisibleItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private val ExtraItemsNearTheSlidingWindow = 100
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
new file mode 100644
index 0000000..01fbb22
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Measures and calculates the positions for the requested items. The result is produced
+ * as a [LazyListMeasureResult] which contains all the calculations.
+ */
+internal fun measureLazyList(
+    itemsCount: Int,
+    itemProvider: LazyMeasuredItemProvider,
+    mainAxisAvailableSize: Int,
+    beforeContentPadding: Int,
+    afterContentPadding: Int,
+    firstVisibleItemIndex: DataIndex,
+    firstVisibleItemScrollOffset: Int,
+    scrollToBeConsumed: Float,
+    constraints: Constraints,
+    isVertical: Boolean,
+    headerIndexes: List<Int>,
+    verticalArrangement: Arrangement.Vertical?,
+    horizontalArrangement: Arrangement.Horizontal?,
+    reverseLayout: Boolean,
+    density: Density,
+    placementAnimator: LazyListItemPlacementAnimator,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): LazyListMeasureResult {
+    require(beforeContentPadding >= 0)
+    require(afterContentPadding >= 0)
+    if (itemsCount <= 0) {
+        // empty data set. reset the current scroll and report zero size
+        return LazyListMeasureResult(
+            firstVisibleItem = null,
+            firstVisibleItemScrollOffset = 0,
+            canScrollForward = false,
+            consumedScroll = 0f,
+            measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+            visibleItemsInfo = emptyList(),
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+            totalItemsCount = 0,
+            reverseLayout = reverseLayout,
+            orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+            afterContentPadding = afterContentPadding
+        )
+    } else {
+        var currentFirstItemIndex = firstVisibleItemIndex
+        var currentFirstItemScrollOffset = firstVisibleItemScrollOffset
+        if (currentFirstItemIndex.value >= itemsCount) {
+            // the data set has been updated and now we have less items that we were
+            // scrolled to before
+            currentFirstItemIndex = DataIndex(itemsCount - 1)
+            currentFirstItemScrollOffset = 0
+        }
+
+        // represents the real amount of scroll we applied as a result of this measure pass.
+        var scrollDelta = scrollToBeConsumed.roundToInt()
+
+        // applying the whole requested scroll offset. we will figure out if we can't consume
+        // all of it later
+        currentFirstItemScrollOffset -= scrollDelta
+
+        // if the current scroll offset is less than minimally possible
+        if (currentFirstItemIndex == DataIndex(0) && currentFirstItemScrollOffset < 0) {
+            scrollDelta += currentFirstItemScrollOffset
+            currentFirstItemScrollOffset = 0
+        }
+
+        // this will contain all the MeasuredItems representing the visible items
+        val visibleItems = mutableListOf<LazyMeasuredItem>()
+
+        // include the start padding so we compose items in the padding area. before starting
+        // scrolling forward we would remove it back
+        currentFirstItemScrollOffset -= beforeContentPadding
+
+        // define min and max offsets (min offset currently includes beforeContentPadding)
+        val minOffset = -beforeContentPadding
+        val maxOffset = mainAxisAvailableSize
+
+        // max of cross axis sizes of all visible items
+        var maxCrossAxis = 0
+
+        // we had scrolled backward or we compose items in the start padding area, which means
+        // items before current firstItemScrollOffset should be visible. compose them and update
+        // firstItemScrollOffset
+        while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
+            val previous = DataIndex(currentFirstItemIndex.value - 1)
+            val measuredItem = itemProvider.getAndMeasure(previous)
+            visibleItems.add(0, measuredItem)
+            maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+            currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+            currentFirstItemIndex = previous
+        }
+        // if we were scrolled backward, but there were not enough items before. this means
+        // not the whole scroll was consumed
+        if (currentFirstItemScrollOffset < minOffset) {
+            scrollDelta += currentFirstItemScrollOffset
+            currentFirstItemScrollOffset = minOffset
+        }
+
+        // neutralize previously added start padding as we stopped filling the before content padding
+        currentFirstItemScrollOffset += beforeContentPadding
+
+        var index = currentFirstItemIndex
+        val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+        var currentMainAxisOffset = -currentFirstItemScrollOffset
+
+        // first we need to skip items we already composed while composing backward
+        visibleItems.fastForEach {
+            index++
+            currentMainAxisOffset += it.sizeWithSpacings
+        }
+
+        // then composing visible items forward until we fill the whole viewport.
+        // we want to have at least one item in visibleItems even if in fact all the items are
+        // offscreen, this can happen if the content padding is larger than the available size.
+        while ((currentMainAxisOffset <= maxMainAxis || visibleItems.isEmpty()) &&
+            index.value < itemsCount
+        ) {
+            val measuredItem = itemProvider.getAndMeasure(index)
+            currentMainAxisOffset += measuredItem.sizeWithSpacings
+
+            if (currentMainAxisOffset <= minOffset && index.value != itemsCount - 1) {
+                // this item is offscreen and will not be placed. advance firstVisibleItemIndex
+                currentFirstItemIndex = index + 1
+                currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
+            } else {
+                maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+                visibleItems.add(measuredItem)
+            }
+
+            index++
+        }
+
+        // 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) {
+            val toScrollBack = maxOffset - currentMainAxisOffset
+            currentFirstItemScrollOffset -= toScrollBack
+            currentMainAxisOffset += toScrollBack
+            while (currentFirstItemScrollOffset < beforeContentPadding &&
+                currentFirstItemIndex > DataIndex(0)
+            ) {
+                val previousIndex = DataIndex(currentFirstItemIndex.value - 1)
+                val measuredItem = itemProvider.getAndMeasure(previousIndex)
+                visibleItems.add(0, measuredItem)
+                maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+                currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+                currentFirstItemIndex = previousIndex
+            }
+            scrollDelta += toScrollBack
+            if (currentFirstItemScrollOffset < 0) {
+                scrollDelta += currentFirstItemScrollOffset
+                currentMainAxisOffset += currentFirstItemScrollOffset
+                currentFirstItemScrollOffset = 0
+            }
+        }
+
+        // report the amount of pixels we consumed. scrollDelta can be smaller than
+        // scrollToBeConsumed if there were not enough items to fill the offered space or it
+        // can be larger if items were resized, or if, for example, we were previously
+        // displaying the item 15, but now we have only 10 items in total in the data set.
+        val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+            abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+        ) {
+            scrollDelta.toFloat()
+        } else {
+            scrollToBeConsumed
+        }
+
+        // the initial offset for items from visibleItems list
+        val visibleItemsScrollOffset = -currentFirstItemScrollOffset
+        var firstItem = visibleItems.first()
+
+        // even if we compose items to fill before content padding we should ignore items fully
+        // located there for the state's scroll position calculation (first item + first offset)
+        if (beforeContentPadding > 0) {
+            for (i in visibleItems.indices) {
+                val size = visibleItems[i].sizeWithSpacings
+                if (currentFirstItemScrollOffset != 0 && size <= currentFirstItemScrollOffset &&
+                    i != visibleItems.lastIndex
+                ) {
+                    currentFirstItemScrollOffset -= size
+                    firstItem = visibleItems[i + 1]
+                } else {
+                    break
+                }
+            }
+        }
+
+        // Compose extra items before or after the visible items.
+        fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
+        fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
+        val extraItemsBefore =
+            if (beyondBoundsInfo.hasIntervals() &&
+                visibleItems.first().index > beyondBoundsInfo.startIndex()) {
+                mutableListOf<LazyMeasuredItem>().apply {
+                    for (i in visibleItems.first().index - 1 downTo beyondBoundsInfo.startIndex()) {
+                        add(itemProvider.getAndMeasure(DataIndex(i)))
+                    }
+                }
+            } else {
+                emptyList()
+            }
+        val extraItemsAfter =
+            if (beyondBoundsInfo.hasIntervals() &&
+                visibleItems.last().index < beyondBoundsInfo.endIndex()) {
+                mutableListOf<LazyMeasuredItem>().apply {
+                    for (i in visibleItems.last().index until beyondBoundsInfo.endIndex()) {
+                        add(itemProvider.getAndMeasure(DataIndex(i + 1)))
+                    }
+                }
+            } else {
+                emptyList()
+            }
+
+        val noExtraItems = firstItem == visibleItems.first() &&
+            extraItemsBefore.isEmpty() &&
+            extraItemsAfter.isEmpty()
+
+        val layoutWidth =
+            constraints.constrainWidth(if (isVertical) maxCrossAxis else currentMainAxisOffset)
+        val layoutHeight =
+            constraints.constrainHeight(if (isVertical) currentMainAxisOffset else maxCrossAxis)
+
+        val positionedItems = calculateItemsOffsets(
+            items = visibleItems,
+            extraItemsBefore = extraItemsBefore,
+            extraItemsAfter = extraItemsAfter,
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            finalMainAxisOffset = currentMainAxisOffset,
+            maxOffset = maxOffset,
+            itemsScrollOffset = visibleItemsScrollOffset,
+            isVertical = isVertical,
+            verticalArrangement = verticalArrangement,
+            horizontalArrangement = horizontalArrangement,
+            reverseLayout = reverseLayout,
+            density = density,
+        )
+
+        val headerItem = if (headerIndexes.isNotEmpty()) {
+            findOrComposeLazyListHeader(
+                composedVisibleItems = positionedItems,
+                itemProvider = itemProvider,
+                headerIndexes = headerIndexes,
+                beforeContentPadding = beforeContentPadding,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight
+            )
+        } else {
+            null
+        }
+
+        placementAnimator.onMeasured(
+            consumedScroll = consumedScroll.toInt(),
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            reverseLayout = reverseLayout,
+            positionedItems = positionedItems,
+            itemProvider = itemProvider
+        )
+
+        return LazyListMeasureResult(
+            firstVisibleItem = firstItem,
+            firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
+            canScrollForward = currentMainAxisOffset > maxOffset,
+            consumedScroll = consumedScroll,
+            measureResult = layout(layoutWidth, layoutHeight) {
+                positionedItems.fastForEach {
+                    if (it !== headerItem) {
+                        it.place(this)
+                    }
+                }
+                // the header item should be placed (drawn) after all other items
+                headerItem?.place(this)
+            },
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = maxOffset + afterContentPadding,
+            visibleItemsInfo = if (noExtraItems) positionedItems else positionedItems.fastFilter {
+                (it.index >= visibleItems.first().index && it.index <= visibleItems.last().index) ||
+                    it === headerItem
+            },
+            totalItemsCount = itemsCount,
+            reverseLayout = reverseLayout,
+            orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+            afterContentPadding = afterContentPadding
+        )
+    }
+}
+
+/**
+ * Calculates [LazyMeasuredItem]s offsets.
+ */
+private fun calculateItemsOffsets(
+    items: List<LazyMeasuredItem>,
+    extraItemsBefore: List<LazyMeasuredItem>,
+    extraItemsAfter: List<LazyMeasuredItem>,
+    layoutWidth: Int,
+    layoutHeight: Int,
+    finalMainAxisOffset: Int,
+    maxOffset: Int,
+    itemsScrollOffset: Int,
+    isVertical: Boolean,
+    verticalArrangement: Arrangement.Vertical?,
+    horizontalArrangement: Arrangement.Horizontal?,
+    reverseLayout: Boolean,
+    density: Density,
+): MutableList<LazyListPositionedItem> {
+    val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+    val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
+    if (hasSpareSpace) {
+        check(itemsScrollOffset == 0)
+    }
+
+    val positionedItems =
+        ArrayList<LazyListPositionedItem>(items.size + extraItemsBefore.size + extraItemsAfter.size)
+
+    if (hasSpareSpace) {
+        require(extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty())
+
+        val itemsCount = items.size
+        fun Int.reverseAware() =
+            if (!reverseLayout) this else itemsCount - this - 1
+
+        val sizes = IntArray(itemsCount) { index ->
+            items[index.reverseAware()].size
+        }
+        val offsets = IntArray(itemsCount) { 0 }
+        if (isVertical) {
+            with(requireNotNull(verticalArrangement)) {
+                density.arrange(mainAxisLayoutSize, sizes, offsets)
+            }
+        } else {
+            with(requireNotNull(horizontalArrangement)) {
+                // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+                density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+            }
+        }
+
+        val reverseAwareOffsetIndices =
+            if (!reverseLayout) offsets.indices else offsets.indices.reversed()
+        for (index in reverseAwareOffsetIndices) {
+            val absoluteOffset = offsets[index]
+            // when reverseLayout == true, offsets are stored in the reversed order to items
+            val item = items[index.reverseAware()]
+            val relativeOffset = if (reverseLayout) {
+                // inverse offset to align with scroll direction for positioning
+                mainAxisLayoutSize - absoluteOffset - item.size
+            } else {
+                absoluteOffset
+            }
+            positionedItems.add(item.position(relativeOffset, layoutWidth, layoutHeight))
+        }
+    } else {
+        var currentMainAxis = itemsScrollOffset
+        extraItemsBefore.fastForEach {
+            currentMainAxis -= it.sizeWithSpacings
+            positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+        }
+
+        currentMainAxis = itemsScrollOffset
+        items.fastForEach {
+            positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += it.sizeWithSpacings
+        }
+
+        extraItemsAfter.fastForEach {
+            positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += it.sizeWithSpacings
+        }
+    }
+    return positionedItems
+}
+
+/**
+ * Returns a list containing only elements matching the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@OptIn(ExperimentalContracts::class)
+internal fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
+    contract { callsInPlace(predicate) }
+    val target = ArrayList<T>(size)
+    fastForEach {
+        if (predicate(it)) target += (it)
+    }
+    return target
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
new file mode 100644
index 0000000..16902ec
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * The result of the measure pass for lazy list layout.
+ */
+internal class LazyListMeasureResult(
+    // properties defining the scroll position:
+    /** The new first visible item.*/
+    val firstVisibleItem: LazyMeasuredItem?,
+    /** The new value for [TvLazyListState.firstVisibleItemScrollOffset].*/
+    val firstVisibleItemScrollOffset: Int,
+    /** True if there is some space available to continue scrolling in the forward direction.*/
+    val canScrollForward: Boolean,
+    /** The amount of scroll consumed during the measure pass.*/
+    val consumedScroll: Float,
+    /** MeasureResult defining the layout.*/
+    measureResult: MeasureResult,
+    // properties representing the info needed for LazyListLayoutInfo:
+    /** see [TvLazyListLayoutInfo.visibleItemsInfo] */
+    override val visibleItemsInfo: List<TvLazyListItemInfo>,
+    /** see [TvLazyListLayoutInfo.viewportStartOffset] */
+    override val viewportStartOffset: Int,
+    /** see [TvLazyListLayoutInfo.viewportEndOffset] */
+    override val viewportEndOffset: Int,
+    /** see [TvLazyListLayoutInfo.totalItemsCount] */
+    override val totalItemsCount: Int,
+    /** see [TvLazyListLayoutInfo.reverseLayout] */
+    override val reverseLayout: Boolean,
+    /** see [TvLazyListLayoutInfo.orientation] */
+    override val orientation: Orientation,
+    /** see [TvLazyListLayoutInfo.afterContentPadding] */
+    override val afterContentPadding: Int
+) : TvLazyListLayoutInfo, MeasureResult by measureResult {
+    override val viewportSize: IntSize
+        get() = IntSize(width, height)
+    override val beforeContentPadding: Int get() = -viewportStartOffset
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
new file mode 100644
index 0000000..8242e4a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible item index and the first
+ * visible item scroll offset.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+internal class LazyListScrollPosition(
+    initialIndex: Int = 0,
+    initialScrollOffset: Int = 0
+) {
+    var index by mutableStateOf(DataIndex(initialIndex))
+
+    var scrollOffset by mutableStateOf(initialScrollOffset)
+        private set
+
+    private var hadFirstNotEmptyLayout = false
+
+    /** The last know key of the item at [index] position. */
+    private var lastKnownFirstItemKey: Any? = null
+
+    /**
+     * Updates the current scroll position based on the results of the last measurement.
+     */
+    fun updateFromMeasureResult(measureResult: LazyListMeasureResult) {
+        lastKnownFirstItemKey = measureResult.firstVisibleItem?.key
+        // we ignore the index and offset from measureResult until we get at least one
+        // measurement with real items. otherwise the initial index and scroll passed to the
+        // state would be lost and overridden with zeros.
+        if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
+            hadFirstNotEmptyLayout = true
+            val scrollOffset = measureResult.firstVisibleItemScrollOffset
+            check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+            Snapshot.withoutReadObservation {
+                update(
+                    DataIndex(measureResult.firstVisibleItem?.index ?: 0),
+                    scrollOffset
+                )
+            }
+        }
+    }
+
+    /**
+     * Updates the scroll position - the passed values will be used as a start position for
+     * composing the items during the next measure pass and will be updated by the real
+     * position calculated during the measurement. This means that there is no guarantee that
+     * exactly this index and offset will be applied as it is possible that:
+     * a) there will be no item at this index in reality
+     * b) item at this index will be smaller than the asked scrollOffset, which means we would
+     * switch to the next item
+     * c) there will be not enough items to fill the viewport after the requested index, so we
+     * would have to compose few elements before the asked index, changing the first visible item.
+     */
+    fun requestPosition(index: DataIndex, scrollOffset: Int) {
+        update(index, scrollOffset)
+        // clear the stored key as we have a direct request to scroll to [index] position and the
+        // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+        lastKnownFirstItemKey = null
+    }
+
+    /**
+     * In addition to keeping the first visible item index we also store the key of this item.
+     * When the user provided custom keys for the items this mechanism allows us to detect when
+     * there were items added or removed before our current first visible item and keep this item
+     * as the first visible one even given that its index has been changed.
+     */
+    @Suppress("IllegalExperimentalApiUsage") // TODO(b/233188423): Address before moving to beta
+    @ExperimentalFoundationApi
+    fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+        Snapshot.withoutReadObservation {
+            update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+        }
+    }
+
+    private fun update(index: DataIndex, scrollOffset: Int) {
+        require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+        if (index != this.index) {
+            this.index = index
+        }
+        if (scrollOffset != this.scrollOffset) {
+            this.scrollOffset = scrollOffset
+        }
+    }
+
+    private companion object {
+        /**
+         * Finds a position of the item with the given key in the lists. This logic allows us to
+         * detect when there were items added or removed before our current first item.
+         */
+        @ExperimentalFoundationApi
+        private fun findLazyListIndexByKey(
+            key: Any?,
+            lastKnownIndex: DataIndex,
+            itemProvider: LazyListItemProvider
+        ): DataIndex {
+            if (key == null) {
+                // there were no real item during the previous measure
+                return lastKnownIndex
+            }
+            if (lastKnownIndex.value < itemProvider.itemCount &&
+                key == itemProvider.getKey(lastKnownIndex.value)
+            ) {
+                // this item is still at the same index
+                return lastKnownIndex
+            }
+            val newIndex = itemProvider.keyToIndexMap[key]
+            if (newIndex != null) {
+                return DataIndex(newIndex)
+            }
+            // fallback to the previous index if we don't know the new index of the item
+            return lastKnownIndex
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
new file mode 100644
index 0000000..b78decf
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.copy
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastSumBy
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.math.abs
+
+private class ItemFoundInScroll(
+    val item: TvLazyListItemInfo,
+    val previousAnimation: AnimationState<Float, AnimationVector1D>
+) : CancellationException()
+
+private val TargetDistance = 2500.dp
+private val BoundDistance = 1500.dp
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("LazyListScrolling: ${generateMsg()}")
+    }
+}
+
+internal suspend fun TvLazyListState.doSmoothScrollToItem(
+    index: Int,
+    scrollOffset: Int
+) {
+    require(index >= 0f) { "Index should be non-negative ($index)" }
+    fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
+        it.index == index
+    }
+    scroll {
+        try {
+            val targetDistancePx = with(density) { TargetDistance.toPx() }
+            val boundDistancePx = with(density) { BoundDistance.toPx() }
+            var loop = true
+            var anim = AnimationState(0f)
+            val targetItemInitialInfo = getTargetItem()
+            if (targetItemInitialInfo != null) {
+                // It's already visible, just animate directly
+                throw ItemFoundInScroll(targetItemInitialInfo, anim)
+            }
+            val forward = index > firstVisibleItemIndex
+
+            fun isOvershot(): Boolean {
+                // Did we scroll past the item?
+                @Suppress("RedundantIf") // It's way easier to understand the logic this way
+                return if (forward) {
+                    if (firstVisibleItemIndex > index) {
+                        true
+                    } else if (
+                        firstVisibleItemIndex == index &&
+                        firstVisibleItemScrollOffset > scrollOffset
+                    ) {
+                        true
+                    } else {
+                        false
+                    }
+                } else { // backward
+                    if (firstVisibleItemIndex < index) {
+                        true
+                    } else if (
+                        firstVisibleItemIndex == index &&
+                        firstVisibleItemScrollOffset < scrollOffset
+                    ) {
+                        true
+                    } else {
+                        false
+                    }
+                }
+            }
+
+            var loops = 1
+            while (loop && layoutInfo.totalItemsCount > 0) {
+                val visibleItems = layoutInfo.visibleItemsInfo
+                val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
+                val indexesDiff = index - firstVisibleItemIndex
+                val expectedDistance = (averageSize * indexesDiff).toFloat() +
+                    scrollOffset - firstVisibleItemScrollOffset
+                val target = if (abs(expectedDistance) < targetDistancePx) {
+                    expectedDistance
+                } else {
+                    if (forward) targetDistancePx else -targetDistancePx
+                }
+
+                debugLog {
+                    "Scrolling to index=$index offset=$scrollOffset from " +
+                        "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
+                        "averageSize=$averageSize and calculated target=$target"
+                }
+
+                anim = anim.copy(value = 0f)
+                var prevValue = 0f
+                anim.animateTo(
+                    target,
+                    sequentialAnimation = (anim.velocity != 0f)
+                ) {
+                    // If we haven't found the item yet, check if it's visible.
+                    var targetItem = getTargetItem()
+
+                    if (targetItem == null) {
+                        // Springs can overshoot their target, clamp to the desired range
+                        val coercedValue = if (target > 0) {
+                            value.coerceAtMost(target)
+                        } else {
+                            value.coerceAtLeast(target)
+                        }
+                        val delta = coercedValue - prevValue
+                        debugLog {
+                            "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
+                        }
+
+                        val consumed = scrollBy(delta)
+                        targetItem = getTargetItem()
+                        if (targetItem != null) {
+                            debugLog { "Found the item after performing scrollBy()" }
+                        } else if (!isOvershot()) {
+                            if (delta != consumed) {
+                                debugLog { "Hit end without finding the item" }
+                                cancelAnimation()
+                                loop = false
+                                return@animateTo
+                            }
+                            prevValue += delta
+                            if (forward) {
+                                if (value > boundDistancePx) {
+                                    debugLog { "Struck bound going forward" }
+                                    cancelAnimation()
+                                }
+                            } else {
+                                if (value < -boundDistancePx) {
+                                    debugLog { "Struck bound going backward" }
+                                    cancelAnimation()
+                                }
+                            }
+
+                            // Magic constants for teleportation chosen arbitrarily by experiment
+                            if (forward) {
+                                if (
+                                    loops >= 2 &&
+                                    index - layoutInfo.visibleItemsInfo.last().index > 100
+                                ) {
+                                    // Teleport
+                                    debugLog { "Teleport forward" }
+                                    snapToItemIndexInternal(index = index - 100, scrollOffset = 0)
+                                }
+                            } else {
+                                if (
+                                    loops >= 2 &&
+                                    layoutInfo.visibleItemsInfo.first().index - index > 100
+                                ) {
+                                    // Teleport
+                                    debugLog { "Teleport backward" }
+                                    snapToItemIndexInternal(index = index + 100, scrollOffset = 0)
+                                }
+                            }
+                        }
+                    }
+
+                    // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
+                    // the final position, there's no need to animate to it.
+                    if (isOvershot()) {
+                        debugLog { "Overshot" }
+                        snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+                        loop = false
+                        cancelAnimation()
+                        return@animateTo
+                    } else if (targetItem != null) {
+                        debugLog { "Found item" }
+                        throw ItemFoundInScroll(targetItem, anim)
+                    }
+                }
+
+                loops++
+            }
+        } catch (itemFound: ItemFoundInScroll) {
+            // We found it, animate to it
+            // Bring to the requested position - will be automatically stopped if not possible
+            val anim = itemFound.previousAnimation.copy(value = 0f)
+            val target = (itemFound.item.offset + scrollOffset).toFloat()
+            var prevValue = 0f
+            debugLog {
+                "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
+            }
+            anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
+                // Springs can overshoot their target, clamp to the desired range
+                val coercedValue = when {
+                    target > 0 -> {
+                        value.coerceAtMost(target)
+                    }
+                    target < 0 -> {
+                        value.coerceAtLeast(target)
+                    }
+                    else -> {
+                        debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
+                        0f
+                    }
+                }
+                val delta = coercedValue - prevValue
+                debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
+                val consumed = scrollBy(delta)
+                if (delta != consumed /* hit the end, stop */ ||
+                    coercedValue != value /* would have overshot, stop */
+                ) {
+                    cancelAnimation()
+                }
+                prevValue += delta
+            }
+            // Once we're finished the animation, snap to the exact position to account for
+            // rounding error (otherwise we tend to end up with the previous item scrolled the
+            // tiniest bit onscreen)
+            // TODO: prevent temporarily scrolling *past* the item
+            snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
new file mode 100644
index 0000000..37862be
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.OnGloballyPositionedModifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlin.math.abs
+
+/**
+ * Creates a [TvLazyListState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [TvLazyListState.firstVisibleItemScrollOffset]
+ */
+@Composable
+fun rememberTvLazyListState(
+    initialFirstVisibleItemIndex: Int = 0,
+    initialFirstVisibleItemScrollOffset: Int = 0
+): TvLazyListState {
+    return rememberSaveable(saver = TvLazyListState.Saver) {
+        TvLazyListState(
+            initialFirstVisibleItemIndex,
+            initialFirstVisibleItemScrollOffset
+        )
+    }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ *
+ * In most cases, this will be created via [rememberTvLazyListState].
+ *
+ * @param firstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [TvLazyListState.firstVisibleItemScrollOffset]
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+class TvLazyListState constructor(
+    firstVisibleItemIndex: Int = 0,
+    firstVisibleItemScrollOffset: Int = 0
+) : ScrollableState {
+    /**
+     * The holder class for the current scroll position.
+     */
+    private val scrollPosition =
+        LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+
+    /**
+     * The index of the first item that is visible.
+     *
+     * Note that this property is observable and if you use it in the composable function it will
+     * be recomposed on every change causing potential performance issues.
+     *
+     * If you want to run some side effects like sending an analytics event or updating a state
+     * based on this value consider using "snapshotFlow":
+     * @sample androidx.compose.foundation.samples.UsingListScrollPositionForSideEffectSample
+     *
+     * If you need to use it in the composition then consider wrapping the calculation into a
+     * derived state in order to only have recompositions when the derived value changes:
+     * @sample androidx.compose.foundation.samples.UsingListScrollPositionInCompositionSample
+     */
+    val firstVisibleItemIndex: Int get() = scrollPosition.index.value
+
+    /**
+     * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the
+     * amount that the item is offset backwards.
+     *
+     * Note that this property is observable and if you use it in the composable function it will
+     * be recomposed on every scroll causing potential performance issues.
+     * @see firstVisibleItemIndex for samples with the recommended usage patterns.
+     */
+    val firstVisibleItemScrollOffset: Int get() = scrollPosition.scrollOffset
+
+    /** Backing state for [layoutInfo] */
+    private val layoutInfoState = mutableStateOf<TvLazyListLayoutInfo>(EmptyLazyListLayoutInfo)
+
+    /**
+     * The object of [TvLazyListLayoutInfo] calculated during the last layout pass. For example,
+     * you can use it to calculate what items are currently visible.
+     *
+     * Note that this property is observable and is updated after every scroll or remeasure.
+     * If you use it in the composable function it will be recomposed on every change causing
+     * potential performance issues including infinity recomposition loop.
+     * Therefore, avoid using it in the composition.
+     *
+     * If you want to run some side effects like sending an analytics event or updating a state
+     * based on this value consider using "snapshotFlow":
+     * @sample androidx.compose.foundation.samples.UsingListLayoutInfoForSideEffectSample
+     */
+    val layoutInfo: TvLazyListLayoutInfo get() = layoutInfoState.value
+
+    /**
+     * [InteractionSource] that will be used to dispatch drag events when this
+     * list is being dragged. If you want to know whether the fling (or animated scroll) is in
+     * progress, use [isScrollInProgress].
+     */
+    val interactionSource: InteractionSource get() = internalInteractionSource
+
+    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+    /**
+     * The amount of scroll to be consumed in the next layout pass.  Scrolling forward is negative
+     * - that is, it is the amount that the items are offset in y
+     */
+    internal var scrollToBeConsumed = 0f
+        private set
+
+    /**
+     * Needed for [animateScrollToItem].  Updated on every measure.
+     */
+    internal var density: Density by mutableStateOf(Density(1f, 1f))
+
+    /**
+     * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+     * we reached the end of the list.
+     */
+    private val scrollableState = ScrollableState { -onScroll(-it) }
+
+    /**
+     * Only used for testing to confirm that we're not making too many measure passes
+     */
+    /*@VisibleForTesting*/
+    internal var numMeasurePasses: Int = 0
+        private set
+
+    /**
+     * Only used for testing to disable prefetching when needed to test the main logic.
+     */
+    /*@VisibleForTesting*/
+    internal var prefetchingEnabled: Boolean = true
+
+    /**
+     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+     */
+    private var indexToPrefetch = -1
+
+    /**
+     * The handle associated with the current index from [indexToPrefetch].
+     */
+    private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+    /**
+     * Keeps the scrolling direction during the previous calculation in order to be able to
+     * detect the scrolling direction change.
+     */
+    private var wasScrollingForward = false
+
+    /**
+     * The [Remeasurement] object associated with our layout. It allows us to remeasure
+     * synchronously during scroll.
+     */
+    internal var remeasurement: Remeasurement? by mutableStateOf(null)
+        private set
+    /**
+     * The modifier which provides [remeasurement].
+     */
+    internal val remeasurementModifier = object : RemeasurementModifier {
+        override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+            this@TvLazyListState.remeasurement = remeasurement
+        }
+    }
+
+    /**
+     * Provides a modifier which allows to delay some interactions (e.g. scroll)
+     * until layout is ready.
+     */
+    internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+    internal var placementAnimator by mutableStateOf<LazyListItemPlacementAnimator?>(null)
+
+    /**
+     * Constraints passed to the prefetcher for premeasuring the prefetched items.
+     */
+    internal var premeasureConstraints by mutableStateOf(Constraints())
+
+    /**
+     * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
+     * pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    suspend fun scrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        scrollOffset: Int = 0
+    ) {
+        scroll {
+            snapToItemIndexInternal(index, scrollOffset)
+        }
+    }
+
+    internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
+        scrollPosition.requestPosition(DataIndex(index), scrollOffset)
+        // placement animation is not needed because we snap into a new position.
+        placementAnimator?.reset()
+        remeasurement?.forceRemeasure()
+    }
+
+    /**
+     * Call this function to take control of scrolling and gain the ability to send scroll events
+     * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
+     * performed within a [scroll] block (even if they don't call any other methods on this
+     * object) in order to guarantee that mutual exclusion is enforced.
+     *
+     * If [scroll] is called from elsewhere, this will be canceled.
+     */
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        awaitLayoutModifier.waitForFirstLayout()
+        scrollableState.scroll(scrollPriority, block)
+    }
+
+    override fun dispatchRawDelta(delta: Float): Float =
+        scrollableState.dispatchRawDelta(delta)
+
+    override val isScrollInProgress: Boolean
+        get() = scrollableState.isScrollInProgress
+
+    private var canScrollBackward: Boolean = false
+    internal var canScrollForward: Boolean = false
+        private set
+
+    // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
+    //  fine-grained control over scrolling
+    /*@VisibleForTesting*/
+    internal fun onScroll(distance: Float): Float {
+        if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+            return 0f
+        }
+        check(abs(scrollToBeConsumed) <= 0.5f) {
+            "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+        }
+        scrollToBeConsumed += distance
+
+        // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+        // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+        // we have less than 0.5 pixels
+        if (abs(scrollToBeConsumed) > 0.5f) {
+            val preScrollToBeConsumed = scrollToBeConsumed
+            remeasurement?.forceRemeasure()
+            if (prefetchingEnabled) {
+                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+            }
+        }
+
+        // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+        if (abs(scrollToBeConsumed) <= 0.5f) {
+            // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+            // that we consumed the whole thing
+            return distance
+        } else {
+            val scrollConsumed = distance - scrollToBeConsumed
+            // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+            // nested scrolling)
+            scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+            return scrollConsumed
+        }
+    }
+
+    private fun notifyPrefetch(delta: Float) {
+        if (!prefetchingEnabled) {
+            return
+        }
+        val info = layoutInfo
+        if (info.visibleItemsInfo.isNotEmpty()) {
+            // check(isActive)
+            val scrollingForward = delta < 0
+            val indexToPrefetch = if (scrollingForward) {
+                info.visibleItemsInfo.last().index + 1
+            } else {
+                info.visibleItemsInfo.first().index - 1
+            }
+            if (indexToPrefetch != this.indexToPrefetch &&
+                indexToPrefetch in 0 until info.totalItemsCount
+            ) {
+                if (wasScrollingForward != scrollingForward) {
+                    // the scrolling direction has been changed which means the last prefetched
+                    // is not going to be reached anytime soon so it is safer to dispose it.
+                    // if this item is already visible it is safe to call the method anyway
+                    // as it will be no-op
+                    currentPrefetchHandle?.cancel()
+                }
+                this.wasScrollingForward = scrollingForward
+                this.indexToPrefetch = indexToPrefetch
+                currentPrefetchHandle = prefetchState.schedulePrefetch(
+                    indexToPrefetch, premeasureConstraints
+                )
+            }
+        }
+    }
+
+    internal val prefetchState = LazyLayoutPrefetchState()
+
+    /**
+     * Animate (smooth scroll) to the given item.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    suspend fun animateScrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        scrollOffset: Int = 0
+    ) {
+        doSmoothScrollToItem(index, scrollOffset)
+    }
+
+    /**
+     *  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
+
+        canScrollForward = result.canScrollForward
+        canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
+            result.firstVisibleItemScrollOffset != 0
+
+        numMeasurePasses++
+    }
+
+    /**
+     * When the user provided custom keys for the items we can try to detect when there were
+     * items added or removed before our current first visible item and keep this item
+     * as the first visible one even given that its index has been changed.
+     */
+    internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+        scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [TvLazyListState].
+         */
+        val Saver: Saver<TvLazyListState, *> = listSaver(
+            save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+            restore = {
+                TvLazyListState(
+                    firstVisibleItemIndex = it[0],
+                    firstVisibleItemScrollOffset = it[1]
+                )
+            }
+        )
+    }
+}
+
+private object EmptyLazyListLayoutInfo : TvLazyListLayoutInfo {
+    override val visibleItemsInfo = emptyList<TvLazyListItemInfo>()
+    override val viewportStartOffset = 0
+    override val viewportEndOffset = 0
+    override val totalItemsCount = 0
+    override val viewportSize = IntSize.Zero
+    override val orientation = Orientation.Vertical
+    override val reverseLayout = false
+    override val beforeContentPadding = 0
+    override val afterContentPadding = 0
+}
+
+internal class AwaitFirstLayoutModifier : OnGloballyPositionedModifier {
+    private var wasPositioned = false
+    private var continuation: Continuation<Unit>? = null
+
+    suspend fun waitForFirstLayout() {
+        if (!wasPositioned) {
+            val oldContinuation = continuation
+            suspendCoroutine<Unit> { continuation = it }
+            oldContinuation?.resume(Unit)
+        }
+    }
+
+    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+        if (!wasPositioned) {
+            wasPositioned = true
+            continuation?.resume(Unit)
+            continuation = null
+        }
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt
new file mode 100644
index 0000000..9698c79
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured item of the lazy list. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+internal class LazyMeasuredItem @ExperimentalFoundationApi constructor(
+    val index: Int,
+    private val placeables: Array<Placeable>,
+    private val isVertical: Boolean,
+    private val horizontalAlignment: Alignment.Horizontal?,
+    private val verticalAlignment: Alignment.Vertical?,
+    private val layoutDirection: LayoutDirection,
+    private val reverseLayout: Boolean,
+    private val beforeContentPadding: Int,
+    private val afterContentPadding: Int,
+    private val placementAnimator: LazyListItemPlacementAnimator,
+    /**
+     * Extra spacing to be added to [size] aside from the sum of the [placeables] size. It
+     * is usually representing the spacing after the item.
+     */
+    private val spacing: Int,
+    /**
+     * The offset which shouldn't affect any calculations but needs to be applied for the final
+     * value passed into the place() call.
+     */
+    private val visualOffset: IntOffset,
+    val key: Any,
+) {
+    /**
+     * Sum of the main axis sizes of all the inner placeables.
+     */
+    val size: Int
+
+    /**
+     * Sum of the main axis sizes of all the inner placeables and [spacing].
+     */
+    val sizeWithSpacings: Int
+
+    /**
+     * Max of the cross axis sizes of all the inner placeables.
+     */
+    val crossAxisSize: Int
+
+    init {
+        var mainAxisSize = 0
+        var maxCrossAxis = 0
+        placeables.forEach {
+            mainAxisSize += if (isVertical) it.height else it.width
+            maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
+        }
+        size = mainAxisSize
+        sizeWithSpacings = size + spacing
+        crossAxisSize = maxCrossAxis
+    }
+
+    /**
+     * Calculates positions for the inner placeables at [offset] main axis position.
+     * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+     */
+    fun position(
+        offset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ): LazyListPositionedItem {
+        val wrappers = mutableListOf<LazyListPlaceableWrapper>()
+        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+        var mainAxisOffset = if (reverseLayout) {
+            mainAxisLayoutSize - offset - size
+        } else {
+            offset
+        }
+        var index = if (reverseLayout) placeables.lastIndex else 0
+        while (if (reverseLayout) index >= 0 else index < placeables.size) {
+            val it = placeables[index]
+            val addIndex = if (reverseLayout) 0 else wrappers.size
+            val placeableOffset = if (isVertical) {
+                val x = requireNotNull(horizontalAlignment)
+                    .align(it.width, layoutWidth, layoutDirection)
+                IntOffset(x, mainAxisOffset)
+            } else {
+                val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
+                IntOffset(mainAxisOffset, y)
+            }
+            mainAxisOffset += if (isVertical) it.height else it.width
+            wrappers.add(
+                addIndex,
+                LazyListPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
+            )
+            if (reverseLayout) index-- else index++
+        }
+        return LazyListPositionedItem(
+            offset = offset,
+            index = this.index,
+            key = key,
+            size = size,
+            sizeWithSpacings = sizeWithSpacings,
+            minMainAxisOffset = -if (!reverseLayout) beforeContentPadding else afterContentPadding,
+            maxMainAxisOffset = mainAxisLayoutSize +
+                if (!reverseLayout) afterContentPadding else beforeContentPadding,
+            isVertical = isVertical,
+            wrappers = wrappers,
+            placementAnimator = placementAnimator,
+            visualOffset = visualOffset
+        )
+    }
+}
+
+internal class LazyListPositionedItem(
+    override val offset: Int,
+    override val index: Int,
+    override val key: Any,
+    override val size: Int,
+    val sizeWithSpacings: Int,
+    private val minMainAxisOffset: Int,
+    private val maxMainAxisOffset: Int,
+    private val isVertical: Boolean,
+    private val wrappers: List<LazyListPlaceableWrapper>,
+    private val placementAnimator: LazyListItemPlacementAnimator,
+    private val visualOffset: IntOffset
+) : TvLazyListItemInfo {
+    val placeablesCount: Int get() = wrappers.size
+
+    fun getOffset(index: Int) = wrappers[index].offset
+
+    fun getMainAxisSize(index: Int) = wrappers[index].placeable.mainAxisSize
+
+    @Suppress("UNCHECKED_CAST")
+    fun getAnimationSpec(index: Int) =
+        wrappers[index].parentData as? FiniteAnimationSpec<IntOffset>?
+
+    val hasAnimations = run {
+        repeat(placeablesCount) { index ->
+            if (getAnimationSpec(index) != null) {
+                return@run true
+            }
+        }
+        false
+    }
+
+    fun place(
+        scope: Placeable.PlacementScope,
+    ) = with(scope) {
+        repeat(placeablesCount) { index ->
+            val placeable = wrappers[index].placeable
+            val minOffset = minMainAxisOffset - placeable.mainAxisSize
+            val maxOffset = maxMainAxisOffset
+            val offset = if (getAnimationSpec(index) != null) {
+                placementAnimator.getAnimatedOffset(
+                    key, index, minOffset, maxOffset, getOffset(index)
+                )
+            } else {
+                getOffset(index)
+            }
+            if (isVertical) {
+                placeable.placeWithLayer(offset + visualOffset)
+            } else {
+                placeable.placeRelativeWithLayer(offset + visualOffset)
+            }
+        }
+    }
+
+    private val Placeable.mainAxisSize get() = if (isVertical) height else width
+}
+
+internal class LazyListPlaceableWrapper(
+    val offset: IntOffset,
+    val placeable: Placeable,
+    val parentData: Any?
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt
new file mode 100644
index 0000000..2cd0037
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away the subcomposition from the measuring logic.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
+    constraints: Constraints,
+    isVertical: Boolean,
+    private val itemProvider: LazyListItemProvider,
+    private val measureScope: LazyLayoutMeasureScope,
+    private val measuredItemFactory: MeasuredItemFactory
+) {
+    // the constraints we will measure child with. the main axis is not restricted
+    val childConstraints = Constraints(
+        maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
+        maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
+    )
+
+    /**
+     * Used to subcompose items of lazy lists. Composed placeables will be measured with the
+     * correct constraints and wrapped into [LazyMeasuredItem].
+     */
+    fun getAndMeasure(index: DataIndex): LazyMeasuredItem {
+        val key = itemProvider.getKey(index.value)
+        val placeables = measureScope.measure(index.value, childConstraints)
+        return measuredItemFactory.createItem(index, key, placeables)
+    }
+
+    /**
+     * Contains the mapping between the key and the index. It could contain not all the items of
+     * the list as an optimization.
+     **/
+    val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+internal fun interface MeasuredItemFactory {
+    fun createItem(
+        index: DataIndex,
+        key: Any,
+        placeables: Array<Placeable>
+    ): LazyMeasuredItem
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
new file mode 100644
index 0000000..1e3bb60
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+// TODO (b/233188423): Address IllegalExperimentalApiUsage before moving to beta
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo", "IllegalExperimentalApiUsage")
+@ExperimentalFoundationApi
+@Composable
+internal fun Modifier.lazyListSemantics(
+    itemProvider: LazyListItemProvider,
+    state: TvLazyListState,
+    coroutineScope: CoroutineScope,
+    isVertical: Boolean,
+    reverseScrolling: Boolean,
+    userScrollEnabled: Boolean
+) = this.then(
+    remember(
+        itemProvider,
+        state,
+        isVertical,
+        reverseScrolling,
+        userScrollEnabled
+    ) {
+        val indexForKeyMapping: (Any) -> Int = { needle ->
+            val key = itemProvider::getKey
+            var result = -1
+            for (index in 0 until itemProvider.itemCount) {
+                if (key(index) == needle) {
+                    result = index
+                    break
+                }
+            }
+            result
+        }
+
+        val accessibilityScrollState = ScrollAxisRange(
+            value = {
+                // This is a simple way of representing the current position without
+                // needing any lazy items to be measured. It's good enough so far, because
+                // screen-readers care mostly about whether scroll position changed or not
+                // rather than the actual offset in pixels.
+                state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+            },
+            maxValue = {
+                if (state.canScrollForward) {
+                    // If we can scroll further, we don't know the end yet,
+                    // but it's upper bounded by #items + 1
+                    itemProvider.itemCount + 1f
+                } else {
+                    // If we can't scroll further, the current value is the max
+                    state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+                }
+            },
+            reverseScrolling = reverseScrolling
+        )
+        val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+            { x, y ->
+                val delta = if (isVertical) {
+                    y
+                } else {
+                    x
+                }
+                coroutineScope.launch {
+                    (state as ScrollableState).animateScrollBy(delta)
+                }
+                // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+                true
+            }
+        } else {
+            null
+        }
+
+        val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+            { index ->
+                require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+                    "Can't scroll to index $index, it is out of " +
+                        "bounds [0, ${state.layoutInfo.totalItemsCount})"
+                }
+                coroutineScope.launch {
+                    state.scrollToItem(index)
+                }
+                true
+            }
+        } else {
+            null
+        }
+
+        val collectionInfo = CollectionInfo(
+            rowCount = if (isVertical) -1 else 1,
+            columnCount = if (isVertical) 1 else -1
+        )
+
+        Modifier.semantics {
+            indexForKey(indexForKeyMapping)
+
+            if (isVertical) {
+                verticalScrollAxisRange = accessibilityScrollState
+            } else {
+                horizontalScrollAxisRange = accessibilityScrollState
+            }
+
+            if (scrollByAction != null) {
+                scrollBy(action = scrollByAction)
+            }
+
+            if (scrollToIndexAction != null) {
+                scrollToIndex(action = scrollToIndexAction)
+            }
+
+            this.collectionInfo = collectionInfo
+        }
+    }
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
new file mode 100644
index 0000000..55274f8
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+/**
+ * Contains useful information about an individual item in lazy lists like [TvLazyColumn]
+ *  or [TvLazyRow].
+ *
+ * @see TvLazyListLayoutInfo
+ */
+interface TvLazyListItemInfo {
+    /**
+     * The index of the item in the list.
+     */
+    val index: Int
+
+    /**
+     * The key of the item which was passed to the item() or items() function.
+     */
+    val key: Any
+
+    /**
+     * The main axis offset of the item in pixels. It is relative to the start of the lazy list container.
+     */
+    val offset: Int
+
+    /**
+     * The main axis size of the item in pixels. Note that if you emit multiple layouts in the composable
+     * slot for the item then this size will be calculated as the sum of their sizes.
+     */
+    val size: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
new file mode 100644
index 0000000..441895d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.annotation.FloatRange
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+
+@Stable
+@TvLazyListScopeMarker
+sealed interface TvLazyListItemScope {
+    /**
+     * Have the content fill the [Constraints.maxWidth] and [Constraints.maxHeight] of the parent
+     * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to
+     * the [maximum width][Constraints.maxWidth] multiplied by [fraction] and the [minimum
+     * height][Constraints.minHeight] to be equal to the [maximum height][Constraints.maxHeight]
+     * multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
+     * make the content fill the whole available space. [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxSize] can't work inside the scrolling layouts as the items are
+     * measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxSize(
+        @FloatRange(from = 0.0, to = 1.0)
+        fraction: Float = 1f
+    ): Modifier
+
+    /**
+     * Have the content fill the [Constraints.maxWidth] of the parent measurement constraints
+     * by setting the [minimum width][Constraints.minWidth] to be equal to the
+     * [maximum width][Constraints.maxWidth] multiplied by [fraction]. Note that, by default, the
+     * [fraction] is 1, so the modifier will make the content fill the whole parent width.
+     * [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxWidth] can't work inside the scrolling horizontally layouts as the
+     * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxWidth(
+        @FloatRange(from = 0.0, to = 1.0)
+        fraction: Float = 1f
+    ): Modifier
+
+    /**
+     * Have the content fill the [Constraints.maxHeight] of the incoming measurement constraints
+     * by setting the [minimum height][Constraints.minHeight] to be equal to the
+     * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
+     * [fraction] is 1, so the modifier will make the content fill the whole parent height.
+     * [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxHeight] can't work inside the scrolling vertically layouts as the
+     * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxHeight(
+        @FloatRange(from = 0.0, to = 1.0)
+        fraction: Float = 1f
+    ): Modifier
+
+    /**
+     * This modifier animates the item placement within the Lazy list.
+     *
+     * When you provide a key via [TvLazyListScope.item]/[TvLazyListScope.items] this modifier will
+     * enable item reordering animations. Aside from item reordering all other position changes
+     * caused by events like arrangement or alignment changes will also be animated.
+     *
+     * @sample androidx.compose.foundation.samples.ItemPlacementAnimationSample
+     *
+     * @param animationSpec a finite animation that will be used to animate the item placement.
+     */
+    @ExperimentalFoundationApi
+    fun Modifier.animateItemPlacement(
+        animationSpec: FiniteAnimationSpec<IntOffset> = spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = IntOffset.VisibilityThreshold
+        )
+    ): Modifier
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
new file mode 100644
index 0000000..64bd81c
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+
+internal class TvLazyListItemScopeImpl : TvLazyListItemScope {
+
+    var maxWidth: Dp by mutableStateOf(Dp.Unspecified)
+    var maxHeight: Dp by mutableStateOf(Dp.Unspecified)
+
+    override fun Modifier.fillParentMaxSize(fraction: Float) = size(
+        maxWidth * fraction,
+        maxHeight * fraction
+    )
+
+    override fun Modifier.fillParentMaxWidth(fraction: Float) =
+        width(maxWidth * fraction)
+
+    override fun Modifier.fillParentMaxHeight(fraction: Float) =
+        height(maxHeight * fraction)
+
+    @Suppress("IllegalExperimentalApiUsage") // TODO(b/233188423): Address before moving to beta
+    @ExperimentalFoundationApi
+    override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
+        this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
+            name = "animateItemPlacement"
+            value = animationSpec
+        }))
+}
+
+private class AnimateItemPlacementModifier(
+    val animationSpec: FiniteAnimationSpec<IntOffset>,
+    inspectorInfo: InspectorInfo.() -> Unit,
+) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
+    override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AnimateItemPlacementModifier) return false
+        return animationSpec != other.animationSpec
+    }
+
+    override fun hashCode(): Int {
+        return animationSpec.hashCode()
+    }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
new file mode 100644
index 0000000..cc31459
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy lists like
+ * [TvLazyColumn] or [TvLazyRow]. For example you can get the list of currently displayed item.
+ *
+ * Use [TvLazyListState.layoutInfo] to retrieve this
+ */
+sealed interface TvLazyListLayoutInfo {
+    /**
+     * The list of [TvLazyListItemInfo] representing all the currently visible items.
+     */
+    val visibleItemsInfo: List<TvLazyListItemInfo>
+
+    /**
+     * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
+     * which would be visible. Usually it is 0, but it can be negative if non-zero [beforeContentPadding]
+     * was applied as the content displayed in the content padding area is still visible.
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportStartOffset: Int
+
+    /**
+     * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
+     * which would be visible. It is the size of the lazy list layout minus [beforeContentPadding].
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportEndOffset: Int
+
+    /**
+     * The total count of items passed to [TvLazyColumn] or [TvLazyRow].
+     */
+    val totalItemsCount: Int
+
+    /**
+     * The size of the viewport in pixels. It is the lazy list layout size including all the
+     * content paddings.
+     */
+    val viewportSize: IntSize
+
+    /**
+     * The orientation of the lazy list.
+     */
+    val orientation: Orientation
+
+    /**
+     * True if the direction of scrolling and layout is reversed.
+     */
+    val reverseLayout: Boolean
+
+    /**
+     * The content padding in pixels applied before the first item in the direction of scrolling.
+     * For example it is a top content padding for LazyColumn with reverseLayout set to false.
+     */
+    val beforeContentPadding: Int
+
+    /**
+     * The content padding in pixels applied after the last item in the direction of scrolling.
+     * For example it is a bottom content padding for LazyColumn with reverseLayout set to false.
+     */
+    val afterContentPadding: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
new file mode 100644
index 0000000..e91c249
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyListScopeImpl : TvLazyListScope {
+
+    private val _intervals = MutableIntervalList<LazyListIntervalContent>()
+    val intervals: IntervalList<LazyListIntervalContent> = _intervals
+
+    private var _headerIndexes: MutableList<Int>? = null
+    val headerIndexes: List<Int> get() = _headerIndexes ?: emptyList()
+
+    override fun items(
+        count: Int,
+        key: ((index: Int) -> Any)?,
+        contentType: (index: Int) -> Any?,
+        itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
+    ) {
+        _intervals.addInterval(
+            count,
+            LazyListIntervalContent(
+                key = key,
+                type = contentType,
+                item = itemContent
+            )
+        )
+    }
+
+    override fun item(
+        key: Any?,
+        contentType: Any?,
+        content: @Composable TvLazyListItemScope.() -> Unit
+    ) {
+        _intervals.addInterval(
+            1,
+            LazyListIntervalContent(
+                key = if (key != null) { _: Int -> key } else null,
+                type = { contentType },
+                item = { content() }
+            )
+        )
+    }
+}
+
+internal class LazyListIntervalContent(
+    val key: ((index: Int) -> Any)?,
+    val type: ((index: Int) -> Any?),
+    val item: @Composable TvLazyListItemScope.(index: Int) -> Unit
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
new file mode 100644
index 0000000..4931977
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+/**
+ * DSL marker used to distinguish between lazy layout scope and the item scope.
+ */
+@DslMarker
+annotation class TvLazyListScopeMarker
\ No newline at end of file
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index 014dcf7..a7156b4c 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
@@ -71,6 +72,7 @@
 androidx {
     name = "Android Wear Tiles Material"
     type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR_TILES
     inceptionYear = "2021"
     description = "Material components library for Android Wear Tiles."
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index f7c230b..1f8a366 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -16,6 +16,7 @@
 
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
+import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
@@ -111,6 +112,7 @@
 androidx {
     name = "Android Wear Tiles Renderer"
     type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR_TILES
     inceptionYear = "2021"
     description = "Android Wear Tiles Renderer components. These components can be used to parse " +
diff --git a/wear/tiles/tiles-testing/build.gradle b/wear/tiles/tiles-testing/build.gradle
index 7d23776..ddc83b8 100644
--- a/wear/tiles/tiles-testing/build.gradle
+++ b/wear/tiles/tiles-testing/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
@@ -61,6 +62,7 @@
 androidx {
     name = "Android Wear Tiles Testing Utilities"
     type = LibraryType.PUBLISHED_TEST_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR_TILES
     inceptionYear = "2021"
     description = "Testing utilities for Android Wear Tiles."
diff --git a/wear/tiles/tiles/build.gradle b/wear/tiles/tiles/build.gradle
index 92fb660..f316baf 100644
--- a/wear/tiles/tiles/build.gradle
+++ b/wear/tiles/tiles/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
@@ -59,6 +60,7 @@
 androidx {
     name = "Android Wear Tiles"
     type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR_TILES
     inceptionYear = "2020"
     description = "Android Wear Tiles"